Sean Anderson
Sean Anderson

Reputation: 29371

Transition flex-grow with dynamic content height

I'm displaying a list of items. Each item has a header and some content. Each item's height expands/collapses when its header is clicked. The height of each item's content is dynamic.

I've got the following code (seen below) which works. However, there is a slight delay between the user clicking on .header and the transition beginning.

This delay appears to be introduced by my use of max-height: min-content. I believe the browser needs a moment to re-calculate the height of the content after the.isCollapsed class is added/removed.

I'm wondering if there's a more correct way to achieve this effect?

If I remove max-height: min-content then flex: 1 on .item causes each item to be the same height when expanded. This is not desired. I want each item's height to fit its contents.

I do not want a solution which requires me to measure the text in JavaScript or similar. The goal is to leverage flexbox to perform the transition without knowing the height of content.

$('.header').click(function() {
  $(this).parent().toggleClass('isCollapsed');
});
*,
*::before,
*::after {
  box-sizing: border-box;
}
html,
body {
  height: 100%;
  margin: 0;
}
ul,
li {
  list-style: none;
  margin: 0;
  padding: 0;
}
.items {
  display: flex;
  flex-direction: column;
  height: 100%;
  flex: 1;
}
.item {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 48px;
  transition: flex-grow 1s;
  flex: 1;
  max-height: min-content;
}
.header {
  display: flex;
  height: 48px;
  padding: 16px;
  align-items: center;
  flex-shrink: 0;
}
.isCollapsed {
  flex-grow: .001;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class='items'>
  <li class='item'>
    <div class='header'>Item A Header</div>
    <div class='content'>Item A Content This content is</br>
      really</br>
      really</br>
      really</br>
      really</br>
      long.
    </div>
  </li>
  <li class='item'>
    <div class='header'>Item B Header</div>
    <div class='content'>
      Item B Content This content is</br>
      short.
    </div>
  </li>
</ul>

Upvotes: 4

Views: 11190

Answers (3)

Steven Gerdes
Steven Gerdes

Reputation: 86

Using flex-box:

let toggleOpen = () => {
  document.querySelector('.wrapper').classList.toggle('is-open');
}
.wrapper {
  display: flex;
}

.inner {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease-out;
}

.wrapper.is-open .inner {
  max-height: 100%;
}

.fillBelow {
 background-color: red;
}
<button onclick="toggleOpen()">toggle collapsed</button>
<div class="wrapper is-open">
 <div>
  <div class="inner">
  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent mi enim, venenatis non facilisis sed, finibus in enim. Sed auctor enim nisl, sit amet feugiat risus blandit vitae.
  </div>
  </div>
</div>
<div class="fillBelow">other content</div>

Using Grids:

let toggleOpen = () => {
  document.querySelector('.wrapper').classList.toggle('is-open');
}
.wrapper {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s ease-out;
}

.wrapper.is-open {
  grid-template-rows: 1fr;
}

.inner {
  overflow: hidden;
}

.fillBelow {
 background-color: red;
}
<button onclick="toggleOpen()">toggle collapsed</button>
<div class="wrapper is-open">
  <div class="inner">
  Lorem ipsum  Nulla lectus diam, sagittis id urna in, tincidunt facilisis sapien. Sed ante turpis, porttitor a diam at, auctor lobortis sapien. Cras ornare dolor sed arcu laoreet volutpat ut tristique ipsum.
  </div>
</div>
<div class="fillBelow">other content</div>

Note: I know this question is about flex-grow, but I believe the intent is closer to "how to animate an accordion with css". This question just happened to be what was my first hit on google. I didn't come up with this but it is from https://keithjgrant.com/posts/2023/04/transitioning-to-height-auto/

Upvotes: 0

Asons
Asons

Reputation: 87303

Here is a version, which has a fallback for browsers that doesn't support min-content.

One can always play with the max-height and transition-duration values to achieve a smoother transition, though to get it perfect, and assumed the content height is not static, I (today) can't see that happen without script.

An odd thing I noted, on Chrome (using min-content), is that the visible transition effect differs in speed on different viewport sizes, which the max-height version doesn't.

$('.header').click(function() {
  $(this).parent().toggleClass('isCollapsed');  
});
*,
*::before,
*::after {
  box-sizing: border-box;
}
html,
body {
  height: 100%;
  margin: 0;
}
ul,
li {
  list-style: none;
  margin: 0;
  padding: 0;
}
.items {
  display: flex;
  flex-direction: column;
  height: 100%;
  flex: 1;
}
.item {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 48px;
  transition: max-height .5s;
  flex: 0 0 auto;
  max-height: 300px;
}
.header {
  display: flex;
  height: 48px;
  padding: 16px;
  align-items: center;
  flex-shrink: 0;
}
.isCollapsed {
  max-height: 48px;
  transition: max-height .2s;
}
@supports (max-height: min-content) {
  .item {
    max-height: min-content;
    transition: flex-grow .6s;
    flex: 1;
  }
  .isCollapsed {
    max-height: min-content;
    flex: .001;
    transition: flex-grow .3s;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class='items'>
  <li class='item'>
    <div class='header'>Item A Header</div>
    <div class='content'>Item A Content This content is<br>
      really<br>
      really<br>
      really<br>
      really<br>
      long.
    </div>
  </li>
  <li class='item'>
    <div class='header'>Item B Header</div>
    <div class='content'>
      Item B Content This content is<br>
      short.
    </div>
  </li>
  <li class='item'>
    <div class='header'>Item C Header</div>
    <div class='content'>Item C Content This content is<br>
      really<br>
      really<br>
      really<br>
      really<br>
      long.
    </div>
  </li>
</ul>

Upvotes: 0

Michael Benjamin
Michael Benjamin

Reputation: 372194

In the following code block there's some room for greater efficiency.

.item {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 48px;
  transition: flex-grow 1s;
  flex: 1;
  max-height: min-content;
}

You're using min-height and flex. But with flex, you don't really need min-height.

Try this instead:

.item {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* min-height: 48px; */
  transition: flex-grow 1s;
  flex: 1 0 48px; /* adjusted */
  max-height: min-content;
}

The delay is gone on my end.

DEMO

I also corrected the <br> tags (</br> is not valid).

Upvotes: 2

Related Questions