4

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>
Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
Sean Anderson
  • 27,963
  • 30
  • 126
  • 237
  • That didn't answer my question at all. You can see from my example code that I am specifying a value of 0.001 for flex-grow and that it animates successfully in my provided example. Please read the question. – Sean Anderson Jan 23 '16 at 21:11
  • There is a delay between user interaction and the animation. If I remove `max-height: min-content` the delay disappears, but the height of each item grows too large / isn't bounded by its content. – Sean Anderson Jan 23 '16 at 21:13
  • Yeah...not sure there is an answer using flexbox here. The delay, as you suggest, is caused by the browser doing the math. – Paulie_D Jan 23 '16 at 21:15
  • I looked at how Bootstrap does their collapsing menu and it appears they measure the text in JS, set the height, transition, then remove the fixed height. Do you know if that's the only viable way of doing this effect currently? I thought Bootstrap's solution might just be for older browser support. I haven't been able to find much information on doing this strictly for evergreen browsers. – Sean Anderson Jan 23 '16 at 21:17
  • The best trick here is to transition it's max-height to a value that will always be more than the content will, and using ease-out, will making it look the best. – Asons Jan 23 '16 at 21:24
  • Yeah I read through that proposed solution here: http://stackoverflow.com/questions/3508605/how-can-i-transition-height-0-to-height-auto-using-css but there were a lot of valid comments on trying to get the easing timing looking correct. I'm going to play around with max-height: fit-content + collapsing the child content a bit. – Sean Anderson Jan 23 '16 at 21:25

3 Answers3

2

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).

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
  • By adding `transition: flex-grow 0.4s;` to the `.isCollapsed` class, it will respond/collapse faster making it look better, at least for the 2:nd item with less content. It looks very good in Chrome but behaves very odd in Firefox and Edge. – Asons Jan 23 '16 at 22:43
  • I'm pretty sure `min-height: 48px` with `flex-basis: auto` is equivalent to `flex-basis: 48px` as `flex-basis: auto` simply refers to `height`. I'll admit it's a better short hand, but functionally equivalent. See the notes under 'content' here: https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis#Values The delay definitely still exists in your demo. It might be helpful to you if you open dev tools and inspect the element. You'll see the `isCollapsed` class appear/disappear well before any transition effect begins. – Sean Anderson Jan 24 '16 at 01:18
  • The `isCollapsed` class appears / disappears simultaneously with the transition. Maybe there's a connection to caching or local hardware processing. Testing on Chrome (which I believe is the only browser that supports `min-content` anyway. http://caniuse.com/#search=min-content) – Michael Benjamin Jan 30 '16 at 19:05
0

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>
Asons
  • 84,923
  • 12
  • 110
  • 165
0

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/