CSS: Expand/collapse animation in `flex` accordion when container size is fixed

55 Views Asked by At

Good afternoon,

I have a need to create HTML/CSS/JS accordion. Requirements: accordion container's height must be fixed (100% body's height) and if panel's content overflows, scrollbar must be inside panel's content div and not inside accordion container itself! I have created a pen: https://codepen.io/lotrmj/pen/bGJEzrp to show general idea. Everything works as intended, but there is no animation...

Is it possible to create animation for expand/collapse action similar to: https://vuetifyjs.com/en/components/expansion-panels/#variant ? Or is it impossible as panel headers and corresponding contents do not have fixed height? If it is possible, could somebody of you help me a bit?

I have done that before with jQuery UI library and it was working, but I am searching for more modern solutions/libraries..

I tried looking for examples but didn't find any with similar problem.

<template>
  <div id="nav">
      <template v-for="i in 5">
        <div class="panel-header">
          <span>PANEL {{i}}</span>
          <button @click="expandOrCollapse(i)">{{i == activePanel ? "Collapse" : "Expand"}}</button>
        </div>
        <div class="panel-content" v-if="i == activePanel">
          <div class="content">
            Very long panel {{i}} content with scrollbar...
          </div>
        </div>
      </template>
  </div>
  <div id="main">MAIN DIV</div>
</template>

<script>
export default {
  data() {
    return {
      activePanel: 2
    };
  },
  methods: {
    expandOrCollapse(i) {
      if (i == this.activePanel) {
        this.activePanel = undefined;
      } else {
        this.activePanel = i;
      }
    }
  }
};
</script>

<style>
  html, body {
    height: 100%;
    margin: 0;
  }
  body {
    display: flex;
  }
  #app {
    flex-grow: 1;
    display: flex;
  }
  #nav {
   width: 400px;
   border-right: 1px solid #ccc;
   overflow: auto;
   display: flex;
   flex-direction: column;
  }
  #main {
   flex-grow: 1;
   display: flex;
   align-items: center;
   justify-content: center;
  }
  .panel-header, .panel-content {
    border-bottom: 1px solid #ddd;
    padding: 5px;
  }
  .panel-header span {
    padding-right: 3px;
  }
  .panel-content {
    overflow: auto;
    flex-grow: 1;
  }
  .content {
    height: 1000px;
  }
</style>
1

There are 1 best solutions below

0
Wongjn On

Using v-if means the element is removed from the DOM, which does not give any chance to show an animation.

You could consider using a CSS grid layout, where each panel header and content occupy a grid row. Then, CSS transition between 0fr and 1fr to hide or show the panel content respectively. We'd also need to remove the padding-top and padding-bottom for the content too when hidden.

Vue.createApp({
  data() {
    return {
      activePanel: 2
    };
  },
  computed: {
    templateRows() {
      return Array(5)
        .fill()
        .flatMap((_, i) => ['max-content', (i + 1) === this.activePanel ? '1fr' : '0fr'])
        .join(' ');
    },
  },
  methods: {
    expandOrCollapse(i) {
      if (i == this.activePanel) {
        this.activePanel = undefined;
      } else {
        this.activePanel = i;
      }
    }
  }
}).mount('#app');
html, body {
  height: 100%;
  margin: 0;
}
body {
  display: flex;
}
#app {
  flex-grow: 1;
  display: flex;
}
#nav {
 width: 400px;
 border-right: 1px solid #ccc;
 overflow: auto;
 display: grid;
 transition: grid-template-rows 500ms;
}
#main {
 flex-grow: 1;
 display: flex;
 align-items: center;
 justify-content: center;
}
.panel-header, .panel-content {
  border-bottom: 1px solid #ddd;
  padding: 5px;
}
.panel-header span {
  padding-right: 3px;
}
.panel-content {
  overflow: auto;
  flex-grow: 1;
  transition: padding-top 500ms, padding-bottom 500ms;
}
.panel-content:not([aria-expanded="true"]) {
  padding-top: 0;
  padding-bottom: 0;
}
.content {
  height: 1000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.21/vue.global.prod.min.js" integrity="sha512-tltvjJD1pUnXVAp0L9id/mcR+zc0xsIKmPMJksEclJ6uEyI8D6eZWpR0jSVWUTXOKcmeBMyND+LQH4ECf/5WKg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div id="app">
  <div id="nav" :style="{ gridTemplateRows: templateRows }">
      <template v-for="i in 5">
        <div class="panel-header">
          <span>PANEL {{i}}</span>
          <button @click="expandOrCollapse(i)">{{i == activePanel ? "Collapse" : "Expand"}}</button>
        </div>
        <div class="panel-content" :aria-expanded="i == activePanel">
          <div class="content">
            Very long panel {{i}} content with scrollbar...
          </div>
        </div>
      </template>
  </div>
  <div id="main">MAIN DIV</div>
</div>