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
  • 307,417
  • 93
  • 525
  • 644
Sean Anderson
  • 26,361
  • 27
  • 120
  • 228
  • 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

2 Answers2

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
  • 307,417
  • 93
  • 525
  • 644
  • 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
  • 81,773
  • 12
  • 93
  • 144