How can I transition height: 0; to height: auto; using CSS?

Viewed 1532553

I am trying to make a <ul> slide down using CSS transitions.

The <ul> starts off at height: 0;. On hover, the height is set to height:auto;. However, this is causing it to simply appear, not transition,

If I do it from height: 40px; to height: auto;, then it will slide up to height: 0;, and then suddenly jump to the correct height.

How else could I do this without using JavaScript?

#child0 {
  height: 0;
  overflow: hidden;
  background-color: #dedede;
  -moz-transition: height 1s ease;
  -webkit-transition: height 1s ease;
  -o-transition: height 1s ease;
  transition: height 1s ease;
}
#parent0:hover #child0 {
  height: auto;
}
#child40 {
  height: 40px;
  overflow: hidden;
  background-color: #dedede;
  -moz-transition: height 1s ease;
  -webkit-transition: height 1s ease;
  -o-transition: height 1s ease;
  transition: height 1s ease;
}
#parent40:hover #child40 {
  height: auto;
}
h1 {
  font-weight: bold;
}
The only difference between the two snippets of CSS is one has height: 0, the other height: 40.
<hr>
<div id="parent0">
  <h1>Hover me (height: 0)</h1>
  <div id="child0">Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>
  </div>
</div>
<hr>
<div id="parent40">
  <h1>Hover me (height: 40)</h1>
  <div id="child40">Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>
  </div>
</div>

41 Answers

My workaround is to transition max-height to the exact content height for a nice smooth animation, then use a transitionEnd callback to set max-height to 9999px so the content can resize freely.

var content = $('#content');
content.inner = $('#content .inner'); // inner div needed to get size of content when closed

// css transition callback
content.on('transitionEnd webkitTransitionEnd transitionend oTransitionEnd msTransitionEnd', function(e){
    if(content.hasClass('open')){
        content.css('max-height', 9999); // try setting this to 'none'... I dare you!
    }
});

$('#toggle').on('click', function(e){
    content.toggleClass('open closed');
    content.contentHeight = content.outerHeight();
    
    if(content.hasClass('closed')){
        
        // disable transitions & set max-height to content height
        content.removeClass('transitions').css('max-height', content.contentHeight);
        setTimeout(function(){
            
            // enable & start transition
            content.addClass('transitions').css({
                'max-height': 0,
                'opacity': 0
            });
            
        }, 10); // 10ms timeout is the secret ingredient for disabling/enabling transitions
        // chrome only needs 1ms but FF needs ~10ms or it chokes on the first animation for some reason
        
    }else if(content.hasClass('open')){  
        
        content.contentHeight += content.inner.outerHeight(); // if closed, add inner height to content height
        content.css({
            'max-height': content.contentHeight,
            'opacity': 1
        });
        
    }
});
.transitions {
    transition: all 0.5s ease-in-out;
    -webkit-transition: all 0.5s ease-in-out;
    -moz-transition: all 0.5s ease-in-out;
}

body {
    font-family:Arial;
    line-height: 3ex;
}
code {
    display: inline-block;
    background: #fafafa;
    padding: 0 1ex;
}
#toggle {
    display:block;
    padding:10px;
    margin:10px auto;
    text-align:center;
    width:30ex;
}
#content {
    overflow:hidden;
    margin:10px;
    border:1px solid #666;
    background:#efefef;
    opacity:1;
}
#content .inner {
    padding:10px;
    overflow:auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<div id="content" class="open">
    <div class="inner">
        <h3>Smooth CSS Transitions Between <code>height: 0</code> and <code>height: auto</code></h3>
        <p>A clever workaround is to use <code>max-height</code> instead of <code>height</code>, and set it to something bigger than your content. Problem is the browser uses this value to calculate transition duration. So if you set it to <code>max-height: 1000px</code> but the content is only 100px high, the animation will be 10x too fast.</p>
        <p>Another option is to measure the content height with JS and transition to that fixed value, but then you have to keep track of the content and manually resize it if it changes.</p>
        <p>This solution is a hybrid of the two - transition to the measured content height, then set it to <code>max-height: 9999px</code> after the transition for fluid content sizing.</p>
    </div>
</div>

<br />

<button id="toggle">Challenge Accepted!</button>

As I post this there are over 30 answers already, but I feel my answer improves on the already accepted answer by jake.

I was not content with the issue that arises from simply using max-height and CSS3 transitions, since as many commenters noted, you have to set your max-height value very close to the actual height or you'll get a delay. See this JSFiddle for an example of that problem.

To get around this (while still using no JavaScript), I added another HTML element that transitions the transform: translateY CSS value.

This means both max-height and translateY are used: max-height allows the element to push down elements below it, while translateY gives the "instant" effect we want. The issue with max-height still exists, but its effect is lessened. This means you can set a much larger height for your max-height value and worry about it less.

The overall benefit is that on the transition back in (the collapse), the user sees the translateY animation immediately, so it doesn't really matter how long the max-height takes.

Solution as Fiddle

body {
  font-family: sans-serif;
}

.toggle {
  position: relative;
  border: 2px solid #333;
  border-radius: 3px;
  margin: 5px;
  width: 200px;
}

.toggle-header {
  margin: 0;
  padding: 10px;
  background-color: #333;
  color: white;
  text-align: center;
  cursor: pointer;
}

.toggle-height {
  background-color: tomato;
  overflow: hidden;
  transition: max-height .6s ease;
  max-height: 0;
}

.toggle:hover .toggle-height {
  max-height: 1000px;
}

.toggle-transform {
  padding: 5px;
  color: white;
  transition: transform .4s ease;
  transform: translateY(-100%);
}

.toggle:hover .toggle-transform {
  transform: translateY(0);
}
<div class="toggle">
  <div class="toggle-header">
    Toggle!
  </div>
  <div class="toggle-height">
    <div class="toggle-transform">
      <p>Content!</p>
      <p>Content!</p>
      <p>Content!</p>
      <p>Content!</p>
    </div>
  </div>
</div>

<div class="toggle">
  <div class="toggle-header">
    Toggle!
  </div>
  <div class="toggle-height">
    <div class="toggle-transform">
      <p>Content!</p>
      <p>Content!</p>
      <p>Content!</p>
      <p>Content!</p>
    </div>
  </div>
</div>

According to MDN Web Docs, auto values have been intentionally excluded from the CSS transitions spec, so instead of height: auto, use height: 100%, top, or the flex property in grid and flex layouts.

Expanding/collapsing an overlay

.grid-container {
  display: grid;
  position: absolute;
}

.content {
  background: aqua;
  height: 0;
  overflow: hidden;
  transition: 1s;
}

span:hover + .grid-container .content {
  height: 100%;
}
<span>Hover over me!</span>

<div class="grid-container">

  <div class="content">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>

</div>

<p>Rest of the page content...</p>

Expanding/collapsing a sliding overlay

.grid-container {
  display: grid;
  position: absolute;
  overflow: hidden;
  pointer-events: none; /* to enable interaction with elements below the container */
}

.content {
  background: aqua;
  pointer-events: auto;
  position: relative;
  top: -100%;
  transition: 1s;
}

span:hover + .grid-container .content {
  top: 0;
}
<span>Hover over me!</span>

<div class="grid-container">

  <div class="content">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>

</div>

<p>Rest of the page content...</p>

Expanding/collapsing in the document flow

html {
  display: grid;
}

body {
  display: flex;
  flex-direction: column;
}

.content {
  background: aqua;
  flex-basis: 0;
  overflow: hidden;
  transition: 1s;
}

span:hover + .content {
  flex: 1;
}
<span>Hover over me!</span>

<div class="content">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>

<p>Rest of the page content...</p>

One sentence solution: Use padding transition. It's enough for most of cases such as accordion, and even better because it's fast due to that the padding value is often not big.

If you want the animation process to be better, just raise the padding value.

.parent{ border-top: #999 1px solid;}
h1{ margin: .5rem; font-size: 1.3rem}
.children {
  height: 0;
  overflow: hidden;
  background-color: #dedede;
  transition: padding .2s ease-in-out, opacity .2s ease-in-out;
  padding: 0 .5rem;
  opacity: 0;
}
.children::before, .children::after{ content: "";display: block;}
.children::before{ margin-top: -2rem;}
.children::after{ margin-bottom: -2rem;}
.parent:hover .children {
  height: auto;
  opacity: 1;
  padding: 2.5rem .5rem;/* 0.5 + abs(-2), make sure it's less than expected min-height */
}
<div class="parent">
  <h1>Hover me</h1>
  <div class="children">Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>Some content
    <br>
  </div>
</div>
<div class="parent">
  <h1>Hover me(long content)</h1>
  <div class="children">Some content
    <br>Some content<br>Some content
    <br>Some content<br>Some content
    <br>Some content<br>Some content
    <br>Some content<br>Some content
    <br>Some content<br>Some content
    <br>
  </div>
</div>
<div class="parent">
  <h1>Hover me(short content)</h1>
  <div class="children">Some content
    <br>Some content
    <br>Some content
    <br>
  </div>
</div>

This solution uses a few techniques:

  • padding-bottom:100% 'hack' where percentages are defined in terms of the current width of the element. More info on this technique.
  • float shrink-wrapping, (necessitating an extra div to apply the float clearing hack)
  • non-semantic use of https://caniuse.com/#feat=css-writing-mode and some transformations to undo it (this allows use of the padding hack above in a vertical context)

The upshot though is that we get performant transitioning using CSS only, and a single transition function to smoothly achieve the transition; the holy grail!

Of course, there's a downside! I can't work out how to control the width at which content gets cut off (overflow:hidden); because of the padding-bottom hack, the width and height are intimately related. There may be a way though, so will come back to it.

https://jsfiddle.net/EoghanM/n1rp3zb4/28/

body {
  padding: 1em;
}

.trigger {
  font-weight: bold;
}

/* .expander is there for float clearing purposes only */
.expander::after {
  content: '';
  display: table;
  clear: both;
}

.outer {
  float: left; /* purpose: shrink to fit content */
  border: 1px solid green;
  overflow: hidden;
}

.inner {
  transition: padding-bottom 0.3s ease-in-out;  /* or whatever crazy transition function you can come up with! */
  padding-bottom: 0%;  /* percentage padding is defined in terms of width. The width at this level is equal to the height of the content */
  height: 0;

  /* unfortunately, change of writing mode has other bad effects like orientation of cursor */
  writing-mode: vertical-rl;
  cursor: default; /* don't want the vertical-text (sideways I-beam) */
  transform: rotate(-90deg) translateX(-100%);  /* undo writing mode */
  transform-origin: 0 0;
  margin: 0;  /* left/right margins here will add to height */
}

.inner > div { white-space: nowrap; }

.expander:hover .inner,  /* to keep open when expanded */
.trigger:hover+.expander .inner {
  padding-bottom: 100%;
}
<div class="trigger">HoverMe</div>
<div class="expander">
  <div class="outer">
    <div class="inner">
      <div>First Item</div>
      <div>Content</div>
      <div>Content</div>
      <div>Content</div>
      <div>Long Content can't be wider than outer height unfortunately</div>
      <div>Last Item</div>
    </div>
  </div>
</div>
<div>
  after content</div>
</div>

You could do this by creating a reverse (collapse) animation with clip-path.

#child0 {
    display: none;
}
#parent0:hover #child0 {
    display: block;
    animation: height-animation;
    animation-duration: 200ms;
    animation-timing-function: linear;
    animation-fill-mode: backwards;
    animation-iteration-count: 1;
    animation-delay: 200ms;
}
@keyframes height-animation {
    0% {
        clip-path: polygon(0% 0%, 100% 0.00%, 100% 0%, 0% 0%);
    }
    100% {
        clip-path: polygon(0% 0%, 100% 0.00%, 100% 100%, 0% 100%);
    }
}
<div id="parent0">
    <h1>Hover me (height: 0)</h1>
    <div id="child0">Some content
        <br>Some content
        <br>Some content
        <br>Some content
        <br>Some content
        <br>Some content
        <br>
    </div>
</div>

I understand the question asks for a solution without JavaScript. But for those interested here's my solution using just a little bit of JS.

ok, so the element's css whose height will change by default is set to height: 0; and when open height: auto;. It also has transition: height .25s ease-out;. But of course the problem is that it won't transition to or from height: auto;

So what i've done is when opening or closing set the height to the scrollHeight property of the element. This new inline style will have higher specificity and override both height: auto; and height: 0; and the transition runs.

When opening i add a transitionend event listener which will run just once then remove the inline style setting it back to height: auto; which will allow the element to resize if necessary, as in this more complex example with sub menus https://codepen.io/ninjabonsai/pen/GzYyVe

When closing i remove the inline style right after the next event loop cycle by using setTimeout with no delay. This means height: auto; is temporarily overridden which allows the transition back to height 0;

const showHideElement = (element, open) => {
  element.style.height = element.scrollHeight + 'px';
  element.classList.toggle('open', open);

  if (open) {
    element.addEventListener('transitionend', () => {
      element.style.removeProperty('height');
    }, {
      once: true
    });
  } else {
    window.setTimeout(() => {
      element.style.removeProperty('height');
    });
  }
}

const menu = document.body.querySelector('#menu');
const list = document.body.querySelector('#menu > ul')

menu.addEventListener('mouseenter', () => showHideElement(list, true));
menu.addEventListener('mouseleave', () => showHideElement(list, false));
#menu > ul {
  height: 0;
  overflow: hidden;
  background-color: #999;
  transition: height .25s ease-out;
}

#menu > ul.open {
  height: auto;
}
<div id="menu">
  <a>hover me</a>
  <ul>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
    <li>item</li>
  </ul>
</div>

Alternate CSS-only solution with line-height, padding, opacity and margin:

body {
  background-color: linen;
}

main {
  background-color: white;
}

[id^="toggle_"] ~ .content {
  line-height: 0;
  opacity: 0;
  padding: 0 .5rem;
  transition: .2s ease-out;
}

[id^="toggle_"] ~ .content > p {
  margin: 0;
    transition: .2s ease-out;
}

[id^="toggle_"]:checked ~ .content   {
  opacity: 1;
  padding: .5rem;
  line-height: 1.5;
}

[id^="toggle_"]:checked ~ .content p  {
    margin-bottom: .75rem;
}

[id^="toggle_"] + label {
  display: flex;
  justify-content: space-between;
  padding: 0.5em 1em;
  background: lightsteelblue;
  border-bottom: 1px solid gray;
  cursor: pointer;
}

[id^="toggle_"] + label:before {
  content: "Show";
}

[id^="toggle_"]:checked + label:before {
  content: "Hide";
}

[id^="toggle_"] + label:after {
  content: "\25BC";
}

[id^="toggle_"]:checked + label:after {
  content: "\25B2";
}
<main>
    <div>
        <input type="checkbox" id="toggle_1" hidden>
        <label for="toggle_1" hidden></label>
        <div class="content">
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dolor neque, commodo quis leo ut, auctor tincidunt mauris. Nunc fringilla tincidunt metus, non gravida lorem condimentum non. Duis ornare purus nisl, at porta arcu eleifend eget. Integer lorem ante, porta vulputate dui ut, blandit tempor tellus. Proin facilisis bibendum diam, sit amet rutrum est feugiat ut. Mauris rhoncus convallis arcu in condimentum. Donec volutpat dui eu mollis vulputate. Nunc commodo lobortis nunc at ultrices. Suspendisse in lobortis diam. Suspendisse eget vestibulum ex.
            </p>
        </div>
    </div>
    <div>
        <input type="checkbox" id="toggle_2" hidden>
        <label for="toggle_2" hidden></label>
        <div class="content">
            <p>
                Maecenas laoreet nunc sit amet nulla ultrices auctor. Vivamus sed nisi vitae nibh condimentum pulvinar eu vel lorem. Sed pretium viverra eros ut facilisis. In ut fringilla magna. Sed a tempor libero. Donec sapien libero, lacinia sed aliquet ut, imperdiet finibus tellus. Nunc tellus lectus, rhoncus in posuere quis, tempus sit amet enim. Morbi et erat ac velit fringilla dignissim. Donec commodo, est id accumsan cursus, diam dui hendrerit nisi, vel hendrerit purus dolor ut risus. Phasellus mattis egestas ipsum sed ullamcorper. In diam ligula, rhoncus vel enim et, imperdiet porta justo. Curabitur vulputate hendrerit nisl, et ultricies diam. Maecenas ac leo a diam cursus ornare nec eu quam.
            </p>
            <p>Sed non vulputate purus, sed consectetur odio. Sed non nibh fringilla, imperdiet odio nec, efficitur ex. Suspendisse ut dignissim enim. Maecenas felis augue, tempor sit amet sem fringilla, accumsan fringilla nibh. Quisque posuere lacus tortor, quis malesuada magna elementum a. Nullam id purus in ante molestie tincidunt. Morbi luctus orci eu egestas dignissim. Sed tincidunt, libero quis scelerisque bibendum, ligula nisi gravida libero, id lacinia nulla leo in elit.
            </p>
            <p>Aenean aliquam risus id consectetur sagittis. Aliquam aliquam nisl eu augue accumsan, vel maximus lorem viverra. Aliquam ipsum dolor, tempor et justo ac, fermentum mattis dui. Etiam at posuere ligula. Vestibulum tortor metus, viverra vitae mi non, laoreet iaculis purus. Praesent vel semper nibh. Curabitur a congue lacus. In et pellentesque lorem. Morbi posuere felis non diam vulputate, non vulputate ex vehicula. Vivamus ultricies, massa id sagittis consequat, sem mauris tincidunt nunc, eu vehicula augue quam ut mauris.
            </p>
        </div>
    </div>
        <div>
        <input type="checkbox" id="toggle_3" hidden>
        <label for="toggle_3" hidden></label>
        <div class="content">
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dolor neque, commodo quis leo ut, auctor tincidunt mauris. Nunc fringilla tincidunt metus, non gravida lorem condimentum non. Duis ornare purus nisl, at porta arcu eleifend eget. Integer lorem ante, porta vulputate dui ut, blandit tempor tellus. Proin facilisis bibendum diam, sit amet rutrum est feugiat ut. Mauris rhoncus convallis arcu in condimentum. Donec volutpat dui eu mollis vulputate. Nunc commodo lobortis nunc at ultrices. Suspendisse in lobortis diam. Suspendisse eget vestibulum ex.
            </p>
            <p>Sed non vulputate purus, sed consectetur odio. Sed non nibh fringilla, imperdiet odio nec, efficitur ex. Suspendisse ut dignissim enim. Maecenas felis augue, tempor sit amet sem fringilla, accumsan fringilla nibh. Quisque posuere lacus tortor, quis malesuada magna elementum a. Nullam id purus in ante molestie tincidunt. Morbi luctus orci eu egestas dignissim. Sed tincidunt, libero quis scelerisque bibendum, ligula nisi gravida libero, id lacinia nulla leo in elit.
            </p>
        </div>
    </div>
</main>

Flexible Height CSS Only Solution

I've stumbled upon a quirky solution using flex behavior. It works in at least Chrome and Firefox.

  • First, the height transition only works between 0 and 100%, two numeric values. Since "auto" is not a numeric value, fractional increments don't exist between 0 and "auto". 100% is a flexible value, so no specific height is required.

  • Second, both the outer container and the inner container of the hidden content must be set to display: flex with flex-direction: column.

  • Third, the outer container must have a height property. It doesn't matter what the height is since the flex behavior is prioritized over the height, so I use a height of 0.

.outer-container { height: 0; display: flex; flex-direction: column; }

.inner-container { display: flex; flex-direction: column; }

.hidden-content { 
    height: 0; 
    opacity: 0; 
    transition: height 1s 0.5s ease-in-out, opacity 0.5s ease-in-out;
    /* transition out: first fade out opacity, then shrink height after a delay equal to the opacity duration */
    }

.trigger:hover + .inner-container > .hidden-content { 
    height: 100%; 
    opacity: 1; 
    transition: height 1s ease-in-out, opacity 0.5s 1s ease-in-out;
    /* transition in: first expand height, then fade in opacity after a delay equal to the height duration */
}
<div class="outer-container">
  <a href="#" class="trigger">Hover to Reveal Inner Container's Hidden Content</a>
  <div class="inner-container">
    <div class="hidden-content">This is hidden content. When triggered by hover, its height transitions from 0 to 100%, which pushes other content in the same container down gradually.</div>
    <div>Within the same container, this other content is pushed down gradually as the hidden content's height transitions from 0 to 100%.</div>
  </div>
</div>

Press the Run Code Snippet button to see the transition in action. It's CSS only, with no specific height.

A lot of answers, some better than other, most using JS. I believe I figured this out in two use-cases that are easy to understand, although one of them only works in Firefox for now.

This works in every browser if you want to toggle an overlay:

.demo01 {
  overflow: hidden;
  position: absolute;
  pointer-events: none;
}
.demo01__content {
  background: lightgray;
  padding: 1rem;
  pointer-events: all;
  transform: translateY(-100%);
  transition: transform 1s, visibility 1s;
  visibility: hidden;
}
:checked ~ .demo01 .demo01__content {
  transform: translateY(0);
  visibility: visible;
}
<input type="checkbox" /> ⬅︎ Toggle
<div>Something before </div>
<div class="demo01">
  <div class="demo01__content">
    This content should…<br />
    toggle! 
  </div>
</div>
<div>Something after </div>

This (for now) works only in Firefox for toggling in the document flow:

.demo02 {
  display: grid;
  grid-template-rows: 0fr;
  overflow: hidden;
  transition: grid-template-rows 1s;
}
.demo02__content {
  align-self: end;
  min-height: 0;
  background: lightgray;
  transition: visibility 1s;
  visibility: hidden;
}
.demo02__padding {
  padding: 1rem;
}
:checked ~ .demo02 {
  grid-template-rows: 1fr;
}
:checked ~ .demo02 .demo02__content {
  visibility: visible;
}
<input type="checkbox" /> ⬅︎ Toggle
<div>Something before </div>
<div class="demo02">
  <div class="demo02__content">
    <div class="demo02__padding">
      This content should…<br />
      toggle! 
    </div>
  </div>
</div>
<div>Something after </div>

I wrote a blog post about these techniques, with links to the Chromium bug that needs fixing for this to be 100% usable.

This isn't exactly a "solution" to the problem, but more of a workaround. It only works as written with text, but can be changed to work with other elements as needed I'm sure.

.originalContent {
    font-size:0px;
    transition:font-size .2s ease-in-out;
}
.show { /* class to add to content */
    font-size:14px;
}

Here is an example: http://codepen.io/overthemike/pen/wzjRKa

Essentially, you set the font-size to 0 and transition that instead of the height, or max-height, or scaleY() etc. at a quick enough pace to get the height to transform to what you want. To transform the actual height with CSS to auto isn't currently possible, but transforming the content within is, hence the font-size transition.

  • Note - there IS javascript in the codepen, but it's only purpose is to add/remove css classes on click for the accordion. This can be done with hidden radio buttons, but I wasn't focused on that, just the height transformation.

I just animated the <li> element instead of the whole container:

<style>
.menu {
    border: solid;
}
.menu ul li {
    height: 0px;
    transition: height 0.3s;
    overflow: hidden;
}
button:hover ~ .wrapper .menu ul li,
button:focus ~ .wrapper .menu ul li,
.menu:hover ul li {
    height: 20px;
}
</style>


<button>Button</button>
<div class="wrapper">
    <div class="menu">
        <ul>
            <li>menuitem</li>
            <li>menuitem</li>
            <li>menuitem</li>
            <li>menuitem</li>
            <li>menuitem</li>
            <li>menuitem</li>
        </ul>
    </div>
</div>

you can add ul: margin 0; to have 0 height.

I combined both max-height and negative margin to achive this animation.

I used max-height: 2000px, but you can push that number to much higher value if needed. I animate the max-height on the expand and the margin on collapse.

The js part is just the click, can be replaced with :hover or checkbox for pure css solution.

There are only 2 problems i can see so far,

  1. The transition-timing is limited. (i added only 2 timings)
  2. If you click again while the dropdown is collapsing, it will jump.

Here's the result

[...document.querySelectorAll('.ab')].forEach(wrapper => {
    wrapper.addEventListener('click', function () {
        this.classList.toggle('active');
    });
});
* {
  margin: 0;
  box-sizing: border-box;
}

.c {
  overflow: hidden;
}

.items {
  width: 100%;
  visibility: hidden;
  max-height: 0;
  margin-bottom: -2000px;
  -webkit-transition: margin 0.6s cubic-bezier(1, 0, 1, 1), max-height 0s 0.6s linear, visibility 0s 0.6s linear;
  transition: margin 0.6s cubic-bezier(1, 0, 1, 1), max-height 0s 0.6s linear, visibility 0s 0.6s linear;
}
.items > * {
  padding: 1rem;
  background-color: #ddd;
  -webkit-transition: background-color 0.6s ease;
  transition: background-color 0.6s ease;
}
.items > *:hover {
  background-color: #eee;
}

.ab {
  padding: 1rem;
  cursor: pointer;
  background: #eee;
}
.ab.active + .c .items {
  max-height: 2000px;
  margin-bottom: 0;
  visibility: visible;
  -webkit-transition: max-height 0.6s cubic-bezier(1, 0, 1, 1);
  transition: max-height 0.6s cubic-bezier(1, 0, 1, 1);
}

.dropdown {
  margin-right: 1rem;
}

.wrapper {
  display: -webkit-box;
  display: flex;
}
<div class="wrapper">
    <div class="dropdown">
        <div class="ab">just text</div>
        <div class="ab">just text</div>
        <div class="ab">dropdown</div>
        <div class="c">
            <div class="items">
                <p>items</p>
                <p>items</p>
                <p>items asl;dk l;kasl;d sa;lk</p>
                <p>items sal;kd</p>
                <p>items</p>
            </div>
        </div>
        <div class="ab">just text</div>
        <div class="ab">just text</div>
    </div>
    
    <div class="dropdown">
        <div class="ab">dropdown</div>
        <div class="c">
            <div class="items">
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
                <p>items</p>
            </div>
        </div>
        <div class="ab">text</div>
    </div>
    
    <div class="dropdown">
        <div class="ab">placeholder</div>
        <div class="ab">dropdown</div>
        <div class="c">
            <div class="items">
                <p>items</p>
                <p>items</p>
            </div>
        </div>
        <div class="ab">placeholder</div>
        <div class="ab">placeholder</div>
        <div class="ab">placeholder</div>
    </div>
</div>
<h1>text to be pushed</h1>

The approach I use is to have everything based on font-size (using em as unit) or at least everything that affects the vertical size of the box we want to animate open. Then if anything isn't in em units (for example a 1px border) I set that to 0 when the box is closed, by targetting all children with *.

What the animation animates is the font-size in % from 0 to 100. Of what? Of a known font-size of the parent.

The rules for it to work are that everything in the animated box must:

  • use % for all font-sizes and line-height
  • use em for everything vertical (like height, margin and padding top/bottom, etc)
  • if using px for something vertical (e.g. border top/bottom) it should be overwritten when the box is closed (e.g. border-top:0;border-bottom:0;)

In a way you can still use pixels as reference unit, by simply setting the wrapper font-size to 100px e.g. #page{font-size:100px;} so if you want 10px anywhere inside you can use 0.1em.

This is not the prettiest thing anyone can write, but hey, these browsers don't give us any beautiful solution to this problem. As soon as the height of the box is unpredictable we have no choice but to get somewhat dirty, and this is the least dirty thing I came up with.

Hover version:

https://jsfiddle.net/xpsfkb07/1/

#page {
  font-size: calc( (35vw + 65vh) / 30); /* just some responsive design as a bonus */
}
#slidebox {
  background-color: #e8e8e8;
  visibility: hidden;
  font-size: 0; /* animated from 0 to 100% */
  opacity: 0; /* optional */
  transition: 0.5s;
}
a#ahover:hover ~ #slidebox {
  visibility: visible;
  font-size: 100%; /* animated from 0 to 100% */
  opacity: 1; /* optional */
}
a#ahover:not(:hover) ~ #slidebox * {
  border-top: 0;
  border-bottom: 0;
  /* Put here anything vertical that uses px as unit, in this case the borders */
}
a#button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 20em;
  height: 3em;
  padding: 0.5em;
  border: 2px solid #0080ff;
  border-radius: 0.4em;
  background-color: #8fbfef;
  color: #404040;
  box-sizing: border-box;
}
#someform {
  margin-top: 1em;
  margin-bottom: 1em;
  padding: 1em 4em 1em 4em;
  background-color: #d8ffd8;
  border: 1px solid #888888;
}
#someform input {
  display: inline-block;
  box-sizing: border-box;
  font-size: 125%;
  padding: 0.5em;
  width: 50%; /* anything horizontal can still be in px or % */
  border-radius: 0.4em;
  border: 1px solid red;
}
<div id=page>
  <a id=ahover href="#">Hover me</a><br>
  Here is the box that slides:
  <div id=slidebox>
    I am the content of the slide box (line1).<br>
    I am the content of the slide box (line2).<br>
    <a id=button href="#">I am some button in the slide box</a><br>
    I am the content of the slide box (line3).<br>
    I am the content of the slide box (line4).
    <div id=someform>
      Some box with a form or anything...<br>
      <input type=text value="Text Box">
    </div>
    I am the content of the slide box (line5).<br>
    I am the content of the slide box (line6).
  </div>
  And this is after the box.
</div>

Class change version:

https://jsfiddle.net/8xzsrfh6/

const switch_ele = document.getElementById('aclass');
switch_ele.addEventListener('click', function(){
    const box_ele = document.getElementById('slidebox');
    box_ele.className = box_ele.className == 'show' ? 'hide' : 'show';
}, true);
#page {
  font-size: calc( (35vw + 65vh) / 30); /* just some responsive design as a bonus */
}
#slidebox {
  background-color: #e8e8e8;
  visibility: hidden;
  font-size: 0; /* animated from 0 to 100% */
  opacity: 0; /* optional */
  transition: .5s;
}
#slidebox.show {
  visibility: visible;
  font-size: 100%; /* animated from 0 to 100% */
  opacity: 1; /* optional */
}
#slidebox.hide * {
  border-top: 0;
  border-bottom: 0;
  /* Put here anything vertical that uses px as unit, in this case the borders */
}
a#button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 20em;
  height: 3em;
  padding: 0.5em;
  border: 2px solid #0080ff;
  border-radius: 0.4em;
  background-color: #8fbfef;
  color: #404040;
  box-sizing: border-box;
}
#someform {
  margin-top: 1em;
  margin-bottom: 1em;
  padding: 1em 4em 1em 4em;
  background-color: #d8ffd8;
  border: 1px solid #888888;
}
#someform input {
  display: inline-block;
  box-sizing: border-box;
  font-size: 125%;
  padding: 0.5em;
  width: 50%; /* anything horizontal can still be in px or % */
  border-radius: 0.4em;
  border: 1px solid red;
}
<div id=page>
  <a id=aclass href="#">Switch class w/ js</a><br>
  Here is the box that slides:
  <div id=slidebox class=hide>
    I am the content of the slide box (line1).<br>
    I am the content of the slide box (line2).<br>
    <a id=button href="#">I am some button in the slide box</a><br>
    I am the content of the slide box (line3).<br>
    I am the content of the slide box (line4).
    <div id=someform>
      Some box with a form or anything...<br>
      <input type=text value="Text Box">
    </div>
    I am the content of the slide box (line5).<br>
    I am the content of the slide box (line6).
  </div>
  And this is after the box.
</div>

I want to add an example about how to expanding/collapsing keeping the doc flow, this example is for React apps using tailwindcss

export function Collapse({ collapsed = true, children }) {
  return (
    <div className="grid">
      <div className="flex flex-col">
        <div className={`transition-all duration-500 overflow-hidden ${collapsed ? 'basis-0' : 'flex-1'}`}>
          {children}
        </div>
      </div>
    </div>
  );
}

To go deep read: https://stackoverflow.com/a/69871346/9226510

Set the height to auto and transition the max-height.

Tested on Chrome v17

div {
  position: absolute;
  width:100%;
  bottom:0px;
  left:0px;

  background:#333;
  color: #FFF;

  max-height:100%; /**/
  height:auto; /**/

  -webkit-transition: all 0.2s ease-in-out;
  -moz-transition: all 0.2s ease-in-out;
  -o-transition: all 0.2s ease-in-out;
  -ms-transition: all 0.2s ease-in-out;
  transition: all 0.2s ease-in-out;
}

.close {
  max-height:0%; /**/
}
Related