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

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?

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

This question is related to css css-transitions

The answer is


Jake's answer to animate the max-height is great, but I found the delay caused by setting a large max-height annoying.

One could move the collapsable content into an inner div and calculate the max height by getting the height of the inner div (via JQuery it'd be the outerHeight()).

$('button').bind('click', function(e) { 
  e.preventDefault();
  w = $('#outer');
  if (w.hasClass('collapsed')) {
    w.css({ "max-height": $('#inner').outerHeight() + 'px' });
  } else {
    w.css({ "max-height": "0px" });
  }
  w.toggleClass('collapsed');
});

Here's a jsfiddle link: http://jsfiddle.net/pbatey/duZpT

Here's a jsfiddle with the absolute minimal amount of code required: http://jsfiddle.net/8ncjjxh8/


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

_x000D_
_x000D_
body {_x000D_
  font-family: sans-serif;_x000D_
}_x000D_
_x000D_
.toggle {_x000D_
  position: relative;_x000D_
  border: 2px solid #333;_x000D_
  border-radius: 3px;_x000D_
  margin: 5px;_x000D_
  width: 200px;_x000D_
}_x000D_
_x000D_
.toggle-header {_x000D_
  margin: 0;_x000D_
  padding: 10px;_x000D_
  background-color: #333;_x000D_
  color: white;_x000D_
  text-align: center;_x000D_
  cursor: pointer;_x000D_
}_x000D_
_x000D_
.toggle-height {_x000D_
  background-color: tomato;_x000D_
  overflow: hidden;_x000D_
  transition: max-height .6s ease;_x000D_
  max-height: 0;_x000D_
}_x000D_
_x000D_
.toggle:hover .toggle-height {_x000D_
  max-height: 1000px;_x000D_
}_x000D_
_x000D_
.toggle-transform {_x000D_
  padding: 5px;_x000D_
  color: white;_x000D_
  transition: transform .4s ease;_x000D_
  transform: translateY(-100%);_x000D_
}_x000D_
_x000D_
.toggle:hover .toggle-transform {_x000D_
  transform: translateY(0);_x000D_
}
_x000D_
<div class="toggle">_x000D_
  <div class="toggle-header">_x000D_
    Toggle!_x000D_
  </div>_x000D_
  <div class="toggle-height">_x000D_
    <div class="toggle-transform">_x000D_
      <p>Content!</p>_x000D_
      <p>Content!</p>_x000D_
      <p>Content!</p>_x000D_
      <p>Content!</p>_x000D_
    </div>_x000D_
  </div>_x000D_
</div>_x000D_
_x000D_
<div class="toggle">_x000D_
  <div class="toggle-header">_x000D_
    Toggle!_x000D_
  </div>_x000D_
  <div class="toggle-height">_x000D_
    <div class="toggle-transform">_x000D_
      <p>Content!</p>_x000D_
      <p>Content!</p>_x000D_
      <p>Content!</p>_x000D_
      <p>Content!</p>_x000D_
    </div>_x000D_
  </div>_x000D_
</div>
_x000D_
_x000D_
_x000D_


The max-height solution from Jake works well, if the hard-coded max-height value supplied is not much bigger than the real height (because otherwise there are undesirable delays and timing problems). On the other hand if the hard-coded value accidentially is not bigger than the real height the element won't open up completely.

The following CSS only solution also requires a hard-coded size that should be bigger than most of the occurring real sizes. However this solution also works if the real size is in some situations bigger than the hard-coded size. In that event the transition might jump a bit, but it will never leave a partially visible element. So this solution could also be used for unknown content, e.g. from a database, where you just know that the content is usually not bigger than x pixels, but there are exceptions.

Idea is to use a negative value for margin-bottom (or margin-top for a slightly diffenrent animation) and to place the content element into a middle element with overflow:hidden. The negative margin of the content element so reduces the height of the middle element.

The following code uses a transition on margin-bottom from -150px to 0px. This alone works fine as long as the content element is not higher than 150px. In addition it uses a transition on max-height for the middle element from 0px to 100%. This finally hides the middle element if the content element is higher than 150px. For max-height the transition is just used to delay its application by a second when closing, not for a smooth visiual effect ( and therefore it can run from 0px to 100%).

CSS:

.content {
  transition: margin-bottom 1s ease-in;
  margin-bottom: -150px;
}
.outer:hover .middle .content {
  transition: margin-bottom 1s ease-out;
  margin-bottom: 0px
}
.middle {
  overflow: hidden;
  transition: max-height .1s ease 1s;
  max-height: 0px
}
.outer:hover .middle {
  transition: max-height .1s ease 0s;
  max-height: 100%
}

HTML:

<div class="outer">
  <div class="middle">
    <div class="content">
      Sample Text
      <br> Sample Text
      <br> Sample Text
      <div style="height:150px">Sample Test of height 150px</div>
      Sample Text
    </div>
  </div>
  Hover Here
</div>

The value for margin bottom should be negative and as close as possible to the real height of the content element. If it('s absoute value) is bigger there are similar delay and timing problems as with the max-height solutions, which however can be limited as long as the hard coded size is not much bigger than the real one. If the absolute value for margin-bottom is smaller than the real height the tansition jumps a bit. In any case after the transition the content element is either fully displayed or fully removed.

For more details see my blog post http://www.taccgl.org/blog/css_transition_display.html#combined_height


Ok, so I think I came up with a super simple answer... no max-height, uses relative positioning, works on li elements, & is pure CSS. I have not tested in anything but Firefox, though judging by the CSS, it should work on all browsers.

FIDDLE: http://jsfiddle.net/n5XfG/2596/

CSS

.wrap { overflow:hidden; }

.inner {
            margin-top:-100%;
    -webkit-transition:margin-top 500ms;
            transition:margin-top 500ms;
}

.inner.open { margin-top:0px; }

HTML

<div class="wrap">
    <div class="inner">Some Cool Content</div>
</div>

I realize this thread is getting old, but it ranks high on certain Google searches so I figure it's worth updating.

You also just get/set the element's own height:

var load_height = document.getElementById('target_box').clientHeight;
document.getElementById('target_box').style.height = load_height + 'px';

You should dump this Javascript immediately after target_box's closing tag in an inline script tag.


I've been looking at this issue for a while today and came across this solution:

Use max-height and dynamically set the max-height based upon the calculated height of the contents of the container

$(obj).children().each(function(index, element) {
   InnerHeight += $(this).height();
});

to animate to full size:

$(obj).removeClass('collapsed').css('max-height', InnerHeight);

to animate to smaller size:

$(obj).removeClass('collapsed').css('max-height', MySmallerHeight);

use CSS3 transition:max-height;

This way you avoid the glitchy looking animation from way to large of a height AND you dont run the risk of clipping your content.


I'm hesitant to post this as it violates the 'no javascript' part of the question; but will do so anyway as it extends upon the yScale answer https://stackoverflow.com/a/17260048/6691 so could be thought of as a fix for the "doesn't actually remove the space" problem with that answer, with the basic scale effect still working when JavaScript is disabled.

One other caveat is that this should not be used with a general 'hover' CSS rule, but should only be used when a transition is triggered by adding a class in the JavaScript, at which point you'd trigger a time-limited execution of the requestAnimationFrame logic. The jsfiddle example below runs continuously in the background for demonsration purposes, which is not suitable for a normal website where there's other things going on.

Here's the demo: http://jsfiddle.net/EoghanM/oa5dprwL/5/

Basically, we use requestAnimationFrame to monitor the resultant height of the box after the scaleY transformation has been applied (el.getBoundingClientRect().height gets this), and then apply a negative margin-bottom on the element to 'eat up' the blank space.

This works with any of the transformation effects; I've added a rotateX-with-perspective demo also.

I haven't included code to maintain any existing margin bottom on the element.


I've recently been transitioning the max-height on the li elements rather than the wrapping ul.

The reasoning is that the delay for small max-heights is far less noticeable (if at all) compared to large max-heights, and I can also set my max-height value relative to the font-size of the li rather than some arbitrary huge number by using ems or rems.

If my font size is 1rem, I'll set my max-height to something like 3rem (to accommodate wrapped text). You can see an example here:

http://codepen.io/mindfullsilence/pen/DtzjE


Short code example:

.slider ul {
  overflow: hidden;
  -webkit-transition: max-height 3.3s ease;
}

.slider.hide ul {
  max-height: 0px;
}

.slider.show ul {
  max-height: 1000px;
}

Here's a solution I just used in combination with jQuery. This works for the following HTML structure:

<nav id="main-nav">
    <ul>
        <li>
            <a class="main-link" href="yourlink.html">Link</a>
            <ul>
                <li><a href="yourlink.html">Sub Link</a></li>
            </ul>
        </li>
    </ul>
</nav>

and the function:

    $('#main-nav li ul').each(function(){
        $me = $(this);

        //Count the number of li elements in this UL
        var liCount = $me.find('li').size(),
        //Multiply the liCount by the height + the margin on each li
            ulHeight = liCount * 28;

        //Store height in the data-height attribute in the UL
        $me.attr("data-height", ulHeight);
    });

You could then use a click function to set and remove the height using css()

$('#main-nav li a.main-link').click(function(){
    //Collapse all submenus back to 0
    $('#main-nav li ul').removeAttr('style');

    $(this).parent().addClass('current');

    //Set height on current submenu to it's height
    var $currentUl = $('li.current ul'),
        currentUlHeight = $currentUl.attr('data-height');
})

CSS:

#main-nav li ul { 
    height: 0;
    position: relative;
    overflow: hidden;
    opacity: 0; 
    filter: alpha(opacity=0); 
    -ms-filter: "alpha(opacity=0)";
    -khtml-opacity: 0; 
    -moz-opacity: 0;
    -webkit-transition: all .6s ease-in-out;
    -moz-transition: all .6s ease-in-out;
    -o-transition: all .6s ease-in-out;
    -ms-transition: all .6s ease-in-out;
    transition: all .6s ease-in-out;
}

#main-nav li.current ul {
    opacity: 1.0; 
    filter: alpha(opacity=100); 
    -ms-filter: "alpha(opacity=100)";
    -khtml-opacity: 1.0; 
    -moz-opacity: 1.0;
}

.ie #main-nav li.current ul { height: auto !important }

#main-nav li { height: 25px; display: block; margin-bottom: 3px }

I've met this problem and found my workaround.

Firstly I tried to use max-height. But there are two problems:

  1. I'm not really sure what is the max-height, since I'm expanding an unknown length text block.
  2. If I set max-height too large, there is a large delay when collapsing.

Then, my workaround:

function toggleBlock(e) {
    var target = goog.dom.getNextElementSibling(e.target);
    if (target.style.height && target.style.height != "0px") { //collapsing
        goog.style.setHeight(target, target.clientHeight);
        setTimeout(function(){
            target.style.height = "0px";
        }, 100);
    } else { //expanding
        target.style.height = "auto";
        //get the actual height
        var height = target.clientHeight;
        target.style.height = "0px";
        setTimeout(function(){
            goog.style.setHeight(target, height);
        }, 100);
        setTimeout(function(){
            //Set this because I have expanding blocks inside expanding blocks
            target.style.height="auto";
        }, 600); //time is set 100 + transition-duration
    }
}

The scss:

div.block {
    height: 0px;
    overflow: hidden;
    @include transition-property(height);
    @include transition-duration(0.5s);
}

I wanted to have a div slide open/close not just vertically, but also horizontally, so it effectively slides open from the top-left corner to the bottom-right corner. When researching, I ran into this question, but the answers are mainly workarounds and approximations. I wanted a more precise approach... Finally I found a way to do it.

Basically my code below achieves this:

  1. smooth transitions starting and ending exact where you want them to be, so an ease in/out is a real ease in/out
  2. No assumptions on heights or widths or maximum heights and widths. No hardcoded numbers
  3. The transition works also after resizing the containing div (either be it through a window resize or programmatically)
  4. the content div is vertically auto-scrolled when the width gets too small (but doesn't do this during transition
  5. No specific structure is needed in the HTML code or CSS. Only a transition definition in one class (.anim)

So here it is:

<style>
.anim { transition: all 2s ease-out; }
#content { background-color: lightgrey; }
</style>

<input type="button" onclick="toggleDiv()" value="Toggle"><br>
<div id="content">
    The contents of my div.<br>
    The contents of my div. Which could be a bit wider.<br>
    The contents of my div. Or could be even a bit more wider.<br>
    The contents of my div.<br>
</div>

<script>
    function initDiv() {
        // put a wrapper around content
        var content = document.getElementById("content");
        var wrapper = document.createElement("DIV"); 

        // wrapper becomes sibling before content
        content.parentNode.insertBefore(wrapper, content);      
        // put content inside
        wrapper.appendChild (content);

        // add transition settings through class
        wrapper.className = "anim";
        wrapper.style.overflow = "hidden";                               

        // make wrapper invisible       
        wrapper.style.height = 0;
        wrapper.style.width = 0;
        wrapper.style.display = "none";

        // add listener to end of transition of wrapper 
        wrapper.addEventListener(
            "transitionend", 
            function (e) {
                // is it truely a transition on the wrapper, and not on a child in the wrapper or content?
                if (e.target == this) {

                    if (this.style.width == "0px") { // it was the end of closing transition
                        // set wrapper to not-displayed, so elements are no longer included in the tabIndex (if set) 
                        this.style.display = "none";

                    } else { // it was the end of opening transition
                        // put width and height of content to 100% so it responds to window resizes while open 
                        // content is always first and only child 
                        this.children[0].style.width = "100%";
                        this.children[0].style.height = "100%";
                        // set overflow-y responsive to window resizing
                        wrapper.style.overflowY = "auto";
                    };
                }; 
            },
            false);
    };
    function toggleDiv() {
        var content = document.getElementById("content");
        var wrapper = content.parentNode;

        if (wrapper.style.width == "0px") { // triggered from closed state
            // set content width to width available to wrapper
            content.style.width = wrapper.parentNode.scrollWidth;
            // make wrapper visible
            wrapper.style.display = "block";
            // set the height to the rendered content height 
            content.style.height = content.scrollHeight;

            // adjust transition duration so that it has a more or less constant speed regardless of size of content
            wrapper.style.transitionDuration = 0.1 + content.clientHeight/200 + "s";
            // set width to maximum avaible and height to rendered content height (triggers transition)
            wrapper.style.width = "100%";
            wrapper.style.height = content.style.height;

        } else { // triggered from opened state
            // content width was set to 100% on transitionend. Make it fixed again to the current available width
            content.style.width = wrapper.parentNode.scrollWidth;
            // same for height, only the rendered content height
            content.style.height = content.scrollHeight;

            wrapper.style.overflowY = "hidden";

            // adjust transition duration again (window -and content- could have been resized)
            wrapper.style.transitionDuration = 0.1 + content.clientHeight/200 + "s";
            // set wrapper size to zero and trigger transition (triggers transition)
            wrapper.style.height = 0;
            wrapper.style.width = 0;
        };
    };

initDiv();
</script>

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.

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


Check out my post on a somewhat related question.

Basically, start at height: 0px;, and let it transition to an exact height computed by JavaScript.

function setInfoHeight() {
  $(window).on('load resize', function() {
    $('.info').each(function () {
      var current = $(this);
      var closed = $(this).height() == 0;
      current.show().height('auto').attr('h', current.height() );
      current.height(closed ? '0' : current.height());
    });
  });

Whenever the page loads/resized, the element with class info will get its h attribute updated. Then a button triggers the style="height: __" to set it to that previously set h value.

function moreInformation() {
  $('.icon-container').click(function() {
    var info = $(this).closest('.dish-header').next('.info'); // Just the one info
    var icon = $(this).children('.info-btn'); // Select the logo

    // Stop any ongoing animation loops. Without this, you could click button 10
    // times real fast, and watch an animation of the info showing and closing
    // for a few seconds after
    icon.stop();
    info.stop();

    // Flip icon and hide/show info
    icon.toggleClass('flip');

    // Metnod 1, animation handled by JS
    // info.slideToggle('slow');

    // Method 2, animation handled by CSS, use with setInfoheight function
    info.toggleClass('active').height(icon.is('.flip') ? info.attr('h') : '0');

  });
};

Here's the styling for the info class.

.info {
  padding: 0 1em;
  line-height: 1.5em;
  display: inline-block;
  overflow: hidden;
  height: 0px;
  transition: height 0.6s, padding 0.6s;
  &.active {
    border-bottom: $thin-line;
    padding: 1em;
  }
}

Styling might not be supported cross-browser. Here is the live example for this code:

CodePen


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%; /**/
}

I was able to do this. I have a .child & a .parent div. The child div fits perfectly within the parent's width/height with absolute positioning. I then animate the translate property to push it's Y value down 100%. Its very smooth animation, no glitches or down sides like any other solution here.

Something like this, pseudo code

.parent{ position:relative; overflow:hidden; } 
/** shown state */
.child {
  position:absolute;top:0;:left:0;right:0;bottom:0;
  height: 100%;
  transition: transform @overlay-animation-duration ease-in-out;
  .translate(0, 0);
}

/** Animate to hidden by sliding down: */
.child.slidedown {
  .translate(0, 100%); /** Translate the element "out" the bottom of it's .scene container "mask" so its hidden */
}

You would specify a height on .parent, in px, %, or leave as auto. This div then masks out the .child div when it slides down.


this is what i've been using.

Basically, i get all children elements height, sum them up, and then set the max-height for the element, overriding the class (you can make you own class, so you can have diferent instances).

Check it out.

                            <!doctype html>
                            <html>

                            <head>
                                <style>
                                    /* OVERFLOW HIDDEN */
                                    .overflowHidden{
                                        overflow: hidden;
                                    }

                                    /* HEIGHT */
                                    .transitionHeight{
                                        -webkit-transition: max-height 250ms ease-in-out;
                                        -moz-transition: max-height 250ms ease-in-out;
                                        -o-transition: max-height 250ms ease-in-out;
                                        -ms-transition: max-height 250ms ease-in-out;
                                        transition: max-height 250ms ease-in-out;
                                    }
                                    .heightAnimOff{
                                        height: auto;
                                        max-height: 0px;
                                    }
                                    .heightAnimOn{
                                        height: auto;
                                        max-height: 20000px;
                                    }

                                </style>
                                <script src="jquery_1.8.3.min.js" type="text/javascript"></script>
                                <script type="text/javascript">
                                    (function($){
                                            $.toggleAnimHeight = function(alvo, velha, nova){
                                                if ( $(alvo).attr("data-maxHeight") != null ){
                                                }else{
                                                    var totalH = 0;
                                                    $(alvo).children().each(function(){
                                                        totalH += $(this).height();
                                                    });
                                                    $(alvo).attr("data-maxHeight", totalH)
                                                    $("head").append('<style> .'+nova+'{ max-height: '+totalH+'px; } </style>');
                                                }           
                                                if ( $(alvo).attr("class").indexOf(nova) == -1 ){
                                                    $(alvo).removeClass(velha);
                                                    $(alvo).addClass(nova);
                                                }else {
                                                    $(alvo).removeClass(nova);
                                                    $(alvo).addClass(velha);
                                                }
                                            }
                                    }(jQuery));
                                </script>
                            </head>

                            <body>
                                <div class="animContainer">
                                    <button onmousedown="$.toggleAnimHeight( $('#target1'), 'heightAnimOff', 'heightAnimOn' );">Anim Toggle</button>
                                    <div id="target1" class="overflowHidden heightAnimOff transitionHeight">
                                        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id pretium enim, quis faucibus urna. Phasellus blandit nisl eget quam mollis vulputate. Sed pulvinar eros vitae neque volutpat, vitae suscipit urna viverra. Etiam rhoncus purus vitae tortor pulvinar, sed vulputate arcu convallis. Sed porta, mi consectetur convallis semper, odio mauris iaculis purus, non tempor purus augue pharetra lorem. Integer dictum lacus arcu. Vivamus metus lorem, fermentum ac egestas ac, ornare non neque. Aenean ullamcorper adipiscing ante, et mollis orci feugiat et.</p>

                                        <p>Praesent pretium sit amet eros et lacinia. Etiam nec neque ullamcorper, sagittis quam vitae, dictum ipsum. Sed volutpat lorem libero, nec commodo magna posuere rutrum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque id erat odio. Sed faucibus sem eu tortor laoreet pulvinar. Praesent pharetra risus eget metus vulputate, eget condimentum leo consequat. Praesent consequat rutrum convallis.</p>

                                        <p>Aenean euismod metus quis libero commodo, tristique cursus odio vestibulum. Donec quis lobortis arcu, eu luctus diam. In eget nisi non mauris commodo elementum. Sed gravida leo consequat, tempus orci eu, facilisis ipsum. Cras interdum sed odio vel tincidunt. Morbi arcu ipsum, ultricies dictum enim quis, varius dignissim massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec semper, magna eu aliquam luctus, leo purus accumsan massa, at auctor dui dolor eu augue. Maecenas ultrices faucibus ante non mattis.</p>

                                        <p>Pellentesque ut est tortor. Quisque adipiscing ac nisi vel interdum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut facilisis ante sollicitudin vehicula ornare. Quisque sagittis diam nibh, ac imperdiet nibh pulvinar eu. Integer at ipsum a purus tristique porttitor vitae in ante. Sed arcu neque, lacinia eu dolor nec, pellentesque interdum tortor. Morbi ornare aliquet aliquam. Aenean egestas, erat vel tempus mollis, est eros iaculis enim, quis fringilla purus tortor sollicitudin erat. Donec ultrices elit metus, sed iaculis mi dignissim vulputate. Donec adipiscing imperdiet porttitor. Sed ac lacus adipiscing, sagittis sem quis, tincidunt metus. Curabitur vitae augue a dolor scelerisque lobortis ut a nisi.</p>

                                        <p>Quisque sollicitudin diam sit amet dui sollicitudin, ac egestas turpis imperdiet. Nullam id dui at lectus ultrices aliquam. Nam non luctus tortor, vitae elementum elit. Nullam id bibendum orci. Aliquam hendrerit nisi vitae tortor mollis, nec aliquam risus malesuada. In scelerisque nisl arcu, sit amet tincidunt libero consequat pharetra. Quisque aliquam consectetur purus nec sollicitudin. Pellentesque consectetur eleifend tortor in blandit. Pellentesque euismod justo sed lectus congue, ut malesuada diam rhoncus. Nulla id tempor odio. Nulla facilisi. Phasellus lacinia neque in nisi congue aliquet. Aliquam malesuada accumsan mauris eget mattis. Maecenas pellentesque, sem sed ultricies ullamcorper, massa enim consectetur magna, eget sagittis lorem leo vel arcu. Cras ultrices nunc id risus commodo laoreet. Proin nisl nulla, elementum ac libero sed, aliquam mollis massa.</p>
                                    </div>
                                </div>
                            </body>

                            </html>

How about instead of height, you use something like below one

#child0 {
  visibility: hidden;
  opacity: 0;
  transition: visibility 0s, opacity 0.5s linear;
  position: absolute;
} 

#parent0:hover #child0 {
  visibility: visible;
  opacity: 1;
  position: relative;
}

Works great as well. Please do add prefixes. Hope this helps someone.

PS: if you still need height 0 to height something black magic, you can add height: 0; to #child0 and then add height: inherit to #parent0:hover #child0. Simultaneously, you can add transition for height individually or just you all.


I got this working by setting the max-height to none, getting the height, re-set the max-height to the calculated height. Works prefectly for me. I got this working for an accordeon menu, with <h5>'s as the toggler expanding the <div> inside the <div>.

JS:

$('h5').click(function(e) {
  $(this).parent('div').addClass('temp_expanded');
  var getheight = ($(this).parent('div').find('div').height());
  $(this).parent('div').removeClass('temp_expanded');
  $(this).parent('div').find('div').css('max-height', getheight);
});

LESS:

div {
> div {
    max-height: 0px;
    overflow: hidden;
    .transition(all 0.3s ease-in-out);
}

&.temp_expanded {
    > div {
        max-height: none;
    }
}

Flexbox Solution

Pros:

  • simple
  • no JS
  • smooth transition

Cons:

  • element needs to be put in a fixed height flex container

The way it works is by always having flex-basis: auto on the element with content, and transitioning flex-grow and flex-shrink instead.

Edit: Improved JS Fiddle inspired by the Xbox One interface.

_x000D_
_x000D_
* {_x000D_
  margin: 0;_x000D_
  padding: 0;_x000D_
  box-sizing: border-box;_x000D_
  transition: 0.25s;_x000D_
  font-family: monospace;_x000D_
}_x000D_
_x000D_
body {_x000D_
  margin: 10px 0 0 10px;_x000D_
}_x000D_
_x000D_
.box {_x000D_
  width: 150px;_x000D_
  height: 150px;_x000D_
  margin: 0 2px 10px 0;_x000D_
  background: #2d333b;_x000D_
  border: solid 10px #20262e;_x000D_
  overflow: hidden;_x000D_
  display: inline-flex;_x000D_
  flex-direction: column;_x000D_
}_x000D_
_x000D_
.space {_x000D_
  flex-basis: 100%;_x000D_
  flex-grow: 1;_x000D_
  flex-shrink: 0;    _x000D_
}_x000D_
_x000D_
p {_x000D_
  flex-basis: auto;_x000D_
  flex-grow: 0;_x000D_
  flex-shrink: 1;_x000D_
  background: #20262e;_x000D_
  padding: 10px;_x000D_
  width: 100%;_x000D_
  text-align: left;_x000D_
  color: white;_x000D_
}_x000D_
_x000D_
.box:hover .space {_x000D_
  flex-grow: 0;_x000D_
  flex-shrink: 1;_x000D_
}_x000D_
  _x000D_
.box:hover p {_x000D_
  flex-grow: 1;_x000D_
  flex-shrink: 0;    _x000D_
}
_x000D_
<div class="box">_x000D_
  <div class="space"></div>_x000D_
  <p>_x000D_
    Super Metroid Prime Fusion_x000D_
  </p>_x000D_
</div>_x000D_
<div class="box">_x000D_
  <div class="space"></div>_x000D_
  <p>_x000D_
    Resident Evil 2 Remake_x000D_
  </p>_x000D_
</div>_x000D_
<div class="box">_x000D_
  <div class="space"></div>_x000D_
  <p>_x000D_
    Yolo The Game_x000D_
  </p>_x000D_
</div>_x000D_
<div class="box">_x000D_
  <div class="space"></div>_x000D_
  <p>_x000D_
    Final Fantasy 7 Remake + All Additional DLC + Golden Tophat_x000D_
  </p>_x000D_
</div>_x000D_
<div class="box">_x000D_
  <div class="space"></div>_x000D_
  <p>_x000D_
    DerpVille_x000D_
  </p>_x000D_
</div>
_x000D_
_x000D_
_x000D_

JS Fiddle


You can transition from height:0 to height:auto providing that you also provide min-height and max-height.

div.stretchy{
    transition: 1s linear;
}

div.stretchy.hidden{
    height: 0;
}

div.stretchy.visible{
    height: auto;
    min-height:40px;
    max-height:400px;
}

The Correct Solution

scrollHeight

You should use document.getElementById(id).style.maxHeight = document.getElementById(id).scrollHeight+"px".

And in the CSS: transition: max-height 1s ease-in-out;


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

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


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;

_x000D_
_x000D_
const showHideElement = (element, open) => {_x000D_
  element.style.height = element.scrollHeight + 'px';_x000D_
  element.classList.toggle('open', open);_x000D_
_x000D_
  if (open) {_x000D_
    element.addEventListener('transitionend', () => {_x000D_
      element.style.removeProperty('height');_x000D_
    }, {_x000D_
      once: true_x000D_
    });_x000D_
  } else {_x000D_
    window.setTimeout(() => {_x000D_
      element.style.removeProperty('height');_x000D_
    });_x000D_
  }_x000D_
}_x000D_
_x000D_
const menu = document.body.querySelector('#menu');_x000D_
const list = document.body.querySelector('#menu > ul')_x000D_
_x000D_
menu.addEventListener('mouseenter', () => showHideElement(list, true));_x000D_
menu.addEventListener('mouseleave', () => showHideElement(list, false));
_x000D_
#menu>ul {_x000D_
  height: 0;_x000D_
  overflow: hidden;_x000D_
  background-color: #999;_x000D_
  transition: height .25s ease-out;_x000D_
}_x000D_
_x000D_
#menu>ul.open {_x000D_
  height: auto;_x000D_
}
_x000D_
<div id="menu">_x000D_
  <a>hover me</a>_x000D_
  <ul>_x000D_
    <li>item</li>_x000D_
    <li>item</li>_x000D_
    <li>item</li>_x000D_
    <li>item</li>_x000D_
    <li>item</li>_x000D_
  </ul>_x000D_
</div>
_x000D_
_x000D_
_x000D_


You can't currently animate on height when one of the heights involved is auto, you have to set two explicit heights.


EDIT: Scroll down for updated answer
I was making a drop down list and saw this Post ... many different answers but I decide to share my drop down list too, ... It's not perfect but at least it will using only css for drop down! I've been using transform:translateY(y) to transform the list to the view ...
You can see more in the test
http://jsfiddle.net/BVEpc/4/
I've placed div behind every li because my drop down list are coming from up and to show them properly this was needed, my div code is:

#menu div {
    transition: 0.5s 1s;
    z-index:-1;
    -webkit-transform:translateY(-100%);
    -webkit-transform-origin: top;
}

and hover is :

#menu > li:hover div {
    transition: 0.5s;
    -webkit-transform:translateY(0);
}

and because ul height is set to the content it can get over your body content that's why I did this for ul:

 #menu ul {
    transition: 0s 1.5s;
    visibility:hidden;
    overflow:hidden;
}

and hover:

#menu > li:hover ul {
     transition:none;
     visibility:visible;
}

the second time after transition is delay and it will get hidden after my drop down list has been closed animately ...
Hope later someone get benefit of this one.

EDIT: I just can't believe ppl actually using this prototype! this drop down menu is only for one sub menu and that's all!! I've updated a better one that can have two sub menu for both ltr and rtl direction with IE 8 support.
Fiddle for LTR
Fiddle for RTL
hopefully someone find this useful in future.


I posted an answer with some JavaScript and got downvoted, so got annoyed and tried again, and have cracked it with CSS only!

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)
  • unsemantic 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/

_x000D_
_x000D_
body {_x000D_
  padding: 1em;_x000D_
}_x000D_
_x000D_
.trigger {_x000D_
  font-weight: bold;_x000D_
}_x000D_
_x000D_
/* .expander is there for float clearing purposes only */_x000D_
.expander::after {_x000D_
  content: '';_x000D_
  display: table;_x000D_
  clear: both;_x000D_
}_x000D_
_x000D_
.outer {_x000D_
  float: left; /* purpose: shrink to fit content */_x000D_
  border: 1px solid green;_x000D_
  overflow: hidden;_x000D_
}_x000D_
_x000D_
.inner {_x000D_
  transition: padding-bottom 0.3s ease-in-out;  /* or whatever crazy transition function you can come up with! */_x000D_
  padding-bottom: 0%;  /* percentage padding is defined in terms of width. The width at this level is equal to the height of the content */_x000D_
  height: 0;_x000D_
_x000D_
  /* unfortunately, change of writing mode has other bad effects like orientation of cursor */_x000D_
  writing-mode: vertical-rl;_x000D_
  cursor: default; /* don't want the vertical-text (sideways I-beam) */_x000D_
  transform: rotate(-90deg) translateX(-100%);  /* undo writing mode */_x000D_
  transform-origin: 0 0;_x000D_
  margin: 0;  /* left/right margins here will add to height */_x000D_
}_x000D_
_x000D_
.inner > div { white-space: nowrap; }_x000D_
_x000D_
.expander:hover .inner,  /* to keep open when expanded */_x000D_
.trigger:hover+.expander .inner {_x000D_
  padding-bottom: 100%;_x000D_
}
_x000D_
<div class="trigger">HoverMe</div>_x000D_
<div class="expander">_x000D_
  <div class="outer">_x000D_
    <div class="inner">_x000D_
      <div>First Item</div>_x000D_
      <div>Content</div>_x000D_
      <div>Content</div>_x000D_
      <div>Content</div>_x000D_
      <div>Long Content can't be wider than outer height unfortunately</div>_x000D_
      <div>Last Item</div>_x000D_
    </div>_x000D_
  </div>_x000D_
</div>_x000D_
<div>_x000D_
  after content</div>_x000D_
</div>
_x000D_
_x000D_
_x000D_


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.


A visual workaround to animating height using CSS3 transitions is to animate the padding instead.

You don't quite get the full wipe effect, but playing around with the transition-duration and padding values should get you close enough. If you don't want to explicitly set height/max-height, this should be what you're looking for.

div {
    height: 0;
    overflow: hidden;
    padding: 0 18px;
    -webkit-transition: all .5s ease;
       -moz-transition: all .5s ease;
            transition: all .5s ease;
}
div.animated {
    height: auto;
    padding: 24px 18px;
}

http://jsfiddle.net/catharsis/n5XfG/17/ (riffed off stephband's above jsFiddle)


The accepted answer works for most cases, but it doesn't work well when your div can vary greatly in height — the animation speed is not dependent on the actual height of the content, and it can look choppy.

You can still perform the actual animation with CSS, but you need to use JavaScript to compute the height of the items, instead of trying to use auto. No jQuery is required, although you may have to modify this a bit if you want compatibility (works in the latest version of Chrome :)).

_x000D_
_x000D_
window.toggleExpand = function(element) {_x000D_
    if (!element.style.height || element.style.height == '0px') { _x000D_
        element.style.height = Array.prototype.reduce.call(element.childNodes, function(p, c) {return p + (c.offsetHeight || 0);}, 0) + 'px';_x000D_
    } else {_x000D_
        element.style.height = '0px';_x000D_
    }_x000D_
}
_x000D_
#menu #list {_x000D_
    height: 0px;_x000D_
    transition: height 0.3s ease;_x000D_
    background: #d5d5d5;_x000D_
    overflow: hidden;_x000D_
}
_x000D_
<div id="menu">_x000D_
    <input value="Toggle list" type="button" onclick="toggleExpand(document.getElementById('list'));">_x000D_
    <ul id="list">_x000D_
        <!-- Works well with dynamically-sized content. -->_x000D_
        <li>item</li>_x000D_
        <li><div style="height: 100px; width: 100px; background: red;"></div></li>_x000D_
        <li>item</li>_x000D_
        <li>item</li>_x000D_
        <li>item</li>_x000D_
    </ul>_x000D_
</div>
_x000D_
_x000D_
_x000D_


This is what works for me:

  .hide{
    max-height: 0px;
    overflow: hidden;
    transition:max-height .5s ease-in-out;
  }

  .show{
    max-height: 150px; // adjust as needed
    transition: max-height .5s ease-in-out;
  }

you need to put them in all your children component and toggle them with jQuery or React state, here's my case (with next.js and styled-components): https://codesandbox.io/s/ol3kl56q9q


You should use scaleY instead.

_x000D_
_x000D_
ul {_x000D_
  background-color: #eee;_x000D_
  transform: scaleY(0);    _x000D_
  transform-origin: top;_x000D_
  transition: transform 0.26s ease;_x000D_
}_x000D_
p:hover ~ ul {_x000D_
  transform: scaleY(1);_x000D_
}
_x000D_
<p>Hover This</p>_x000D_
<ul>_x000D_
  <li>Coffee</li>_x000D_
  <li>Tea</li>_x000D_
  <li>Milk</li>_x000D_
</ul>
_x000D_
_x000D_
_x000D_

I've made a vendor prefixed version of the above code on jsfiddle, and changed your jsfiddle to use scaleY instead of 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

_x000D_
_x000D_
[...document.querySelectorAll('.ab')].forEach(wrapper => {
    wrapper.addEventListener('click', function () {
        this.classList.toggle('active');
    });
});
_x000D_
* {
  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;
}
_x000D_
<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>
_x000D_
_x000D_
_x000D_


LITTLE JAVASCRIPT + SCSS SOLUTION

I usually use a quite different point of view and a (very) little javascript. The thing is:

  • what we really want is change height

  • The height is the sum of all list items inside the submenu

  • We usually know the height of a list item, since we're styling it

So my solution applies to 'normal' submenus where items names have only 1 row. Anyway, with a little more js one could accomodate even more than 1 row names.

Basically, what I do is simply count the submenus items and apply specific classes accordingly. Then pass the ball to (s)css. So, for example:

var main_menu = $('.main-menu');
var submenus = $('.main-menu').find('.submenu');
submenus.each(function(index,item){
   var i = $(item);
   i.addClass('has-' + i.find('li').length + '-children');
});

You can use any class/selector, obviously. At this point we have submenus like this:

<ul class="submenu has-3-children">
   <li></li>
   <li></li>
   <li></li>
</ul>

And our css like this:

.submenu{
   //your styles [...]
   height:0;
   overflow:hidden;
   transition: all 200ms ease-in-out; //assume Autoprefixer is used
}

We will also have some scss variables like these (arbitrary example):

$sub_item_height:30px;
$sub_item_border:2px;

At this point, assumed that opened main menu items will get a class like 'opened' or the like (your implemetations..), we can do something like this:

//use a number of children reasonably high so it won't be overcomed by real buttons
.main-menu .opened .submenu{
   &.has-1-children{ height:   $sub_item_height*1  + $sub_item_border*1;  }
   &.has-2-children{ height:   $sub_item_height*2  + $sub_item_border*2;  }
   &.has-3-children{ height:   $sub_item_height*3  + $sub_item_border*3;  }
   //and so on....
}

Or, to shorten up:

.main-menu .opened .submenu{
   @for $i from 1 through 12{//12 is totally arbitrary
      &.has-#{$i}-children { height: $menu_item_height * $i + $menu_item_border * $i; }
   }
}

For most of the times, this will do the job. Hope it helps!


Source

This is so late, but for the sake of future researchers, I'll post my answer. I believe most of you looking for height : 0 is for the sake of td or tr toggle transition animation or something similar. But it is not possible to make it using just height, max-height, line-height on td or tr, but you can use the following tricks to make it:

  • Wrapping all td contents into div and use height: 0 + overflow: hidden + white-space: nowrap on divs , and the animation/transition of your choice
  • Use transform: scaleY ( ?° ?? ?°)

_x000D_
_x000D_
.menu {_x000D_
    margin: 0 auto;_x000D_
    padding: 0;_x000D_
    list-style: none;_x000D_
    text-align: center;_x000D_
    max-width: 300px;_x000D_
    width: 100%;_x000D_
}_x000D_
_x000D_
.menu li {_x000D_
    display: block;_x000D_
    margin-bottom: 5px;_x000D_
}_x000D_
.menu li a {_x000D_
    color: #333;_x000D_
    display: inline-block;_x000D_
    font-size: 20px;_x000D_
    line-height: 28px;_x000D_
    font-weight: 500;_x000D_
    font-family: "Poppins", sans-serif;_x000D_
    transition: all 0.5s;_x000D_
    margin: 0 0 10px;_x000D_
}_x000D_
_x000D_
.menu li.submenu .submenu_item {_x000D_
    margin: 0;_x000D_
    padding: 0;_x000D_
    width: 100%;_x000D_
    max-height: 0;_x000D_
    overflow: hidden;_x000D_
    display: flex;_x000D_
    flex-direction: row;_x000D_
    text-align: center;_x000D_
    flex-wrap: wrap;_x000D_
    justify-content: center;_x000D_
    align-items: center;_x000D_
    transition: max-height 1s ease-out !important;_x000D_
    transition-delay: 0s !important;_x000D_
}_x000D_
_x000D_
.menu li.submenu:hover .submenu_item {_x000D_
    max-height: 1000px;_x000D_
    transition: max-height 2s ease-in !important;_x000D_
}_x000D_
_x000D_
.menu li.submenu .submenu_item li {_x000D_
    margin-bottom: 0;_x000D_
    width: 100%;_x000D_
    display: block;_x000D_
    margin: 0;_x000D_
    padding: 0;_x000D_
    list-style: none;_x000D_
    position: relative;_x000D_
}
_x000D_
<p>First you should complete markup  like this</p>_x000D_
_x000D_
<ul class="menu">_x000D_
  <li class="submenu">_x000D_
    <a href="#">Home</a>_x000D_
    <ul class="submenu_item">_x000D_
        <li><a href="index.html">Default</a></li>_x000D_
        <li><a href="index-2.html">Particle</a></li>_x000D_
        <li><a href="index-3.html">Youtube Video</a></li>_x000D_
        <li><a href="index-4.html">Self Hosted Video</a></li>_x000D_
        <li><a href="index-5.html">Slideshow</a></li>_x000D_
    </ul>_x000D_
  </li>_x000D_
</ul>
_x000D_
_x000D_
_x000D_


Here's a way to transition from any starting height, including 0, to auto (full size and flexible) without requiring hard-set code on a per-node basis or any user-code to initialize: https://github.com/csuwildcat/transition-auto. This is basically the holy grail for what you want, I believe --> http://codepen.io/csuwldcat/pen/kwsdF. Just slap the following JS file into your page, and all you need to do after that is add/remove a single boolean attribute - reveal="" - from the nodes you want to expand and contract.

Here's all you need to do as the user, once you include the code block found below the example code:

/*** Nothing out of the ordinary in your styles ***/
<style>
    div {
        height: 0;
        overflow: hidden;
        transition: height 1s;
    }
</style>

/*** Just add and remove one attribute and transition to/from auto! ***/

<div>
    I have tons of content and I am 0px in height you can't see me...
</div>

<div reveal>
     I have tons of content and I am 0px in height you can't see me...
     but now that you added the 'reveal' attribute, 
     I magically transitioned to full height!...
</div>

Here's the code block to include in your page, after that, it's all gravy:

Drop this JS file in your page - it all Just Worksâ„¢

/* Code for height: auto; transitioning */

(function(doc){

/* feature detection for browsers that report different values for scrollHeight when an element's overflow is hidden vs visible (Firefox, IE) */
var test = doc.documentElement.appendChild(doc.createElement('x-reveal-test'));
    test.innerHTML = '-';
    test.style.cssText = 'display: block !important; height: 0px !important; padding: 0px !important; font-size: 0px !important; border-width: 0px !important; line-height: 1px !important; overflow: hidden !important;';
var scroll = test.scrollHeight || 2;
doc.documentElement.removeChild(test);

var loading = true,
    numReg = /^([0-9]*\.?[0-9]*)(.*)/,
    skipFrame = function(fn){
      requestAnimationFrame(function(){
        requestAnimationFrame(fn);
      });
    },
    /* 2 out of 3 uses of this function are purely to work around Chrome's catastrophically busted implementation of auto value CSS transitioning */
    revealFrame = function(el, state, height){
        el.setAttribute('reveal-transition', 'frame');
        el.style.height = height;
        skipFrame(function(){
            el.setAttribute('reveal-transition', state);
            el.style.height = '';
        });
    },
    transitionend = function(e){
      var node = e.target;
      if (node.hasAttribute('reveal')) {
        if (node.getAttribute('reveal-transition') == 'running') revealFrame(node, 'complete', '');
      } 
      else {
        node.removeAttribute('reveal-transition');
        node.style.height = '';
      }
    },
    animationstart = function(e){
      var node = e.target,
          name = e.animationName;   
      if (name == 'reveal' || name == 'unreveal') {

        if (loading) return revealFrame(node, 'complete', 'auto');

        var style = getComputedStyle(node),
            offset = (Number(style.paddingTop.match(numReg)[1])) +
                     (Number(style.paddingBottom.match(numReg)[1])) +
                     (Number(style.borderTopWidth.match(numReg)[1])) +
                     (Number(style.borderBottomWidth.match(numReg)[1]));

        if (name == 'reveal'){
          node.setAttribute('reveal-transition', 'running');
          node.style.height = node.scrollHeight - (offset / scroll) + 'px';
        }
        else {
            if (node.getAttribute('reveal-transition') == 'running') node.style.height = '';
            else revealFrame(node, 'running', node.scrollHeight - offset + 'px');
        }
      }
    };

doc.addEventListener('animationstart', animationstart, false);
doc.addEventListener('MSAnimationStart', animationstart, false);
doc.addEventListener('webkitAnimationStart', animationstart, false);
doc.addEventListener('transitionend', transitionend, false);
doc.addEventListener('MSTransitionEnd', transitionend, false);
doc.addEventListener('webkitTransitionEnd', transitionend, false);

/*
    Batshit readyState/DOMContentLoaded code to dance around Webkit/Chrome animation auto-run weirdness on initial page load.
    If they fixed their code, you could just check for if(doc.readyState != 'complete') in animationstart's if(loading) check
*/
if (document.readyState == 'complete') {
    skipFrame(function(){
        loading = false;
    });
}
else document.addEventListener('DOMContentLoaded', function(e){
    skipFrame(function(){
        loading = false;
    });
}, false);

/* Styles that allow for 'reveal' attribute triggers */
var styles = doc.createElement('style'),
    t = 'transition: none; ',
    au = 'animation: reveal 0.001s; ',
    ar = 'animation: unreveal 0.001s; ',
    clip = ' { from { opacity: 0; } to { opacity: 1; } }',
    r = 'keyframes reveal' + clip,
    u = 'keyframes unreveal' + clip;

styles.textContent = '[reveal] { -ms-'+ au + '-webkit-'+ au +'-moz-'+ au + au +'}' +
    '[reveal-transition="frame"] { -ms-' + t + '-webkit-' + t + '-moz-' + t + t + 'height: auto; }' +
    '[reveal-transition="complete"] { height: auto; }' +
    '[reveal-transition]:not([reveal]) { -webkit-'+ ar +'-moz-'+ ar + ar +'}' +
    '@-ms-' + r + '@-webkit-' + r + '@-moz-' + r + r +
    '@-ms-' + u +'@-webkit-' + u + '@-moz-' + u + u;

doc.querySelector('head').appendChild(styles);

})(document);

/* Code for DEMO */

    document.addEventListener('click', function(e){
      if (e.target.nodeName == 'BUTTON') {
        var next = e.target.nextElementSibling;
        next.hasAttribute('reveal') ? next.removeAttribute('reveal') : next.setAttribute('reveal', '');
      }
    }, false);

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.

_x000D_
_x000D_
var content = $('#content');_x000D_
content.inner = $('#content .inner'); // inner div needed to get size of content when closed_x000D_
_x000D_
// css transition callback_x000D_
content.on('transitionEnd webkitTransitionEnd transitionend oTransitionEnd msTransitionEnd', function(e){_x000D_
    if(content.hasClass('open')){_x000D_
        content.css('max-height', 9999); // try setting this to 'none'... I dare you!_x000D_
    }_x000D_
});_x000D_
_x000D_
$('#toggle').on('click', function(e){_x000D_
    content.toggleClass('open closed');_x000D_
    content.contentHeight = content.outerHeight();_x000D_
    _x000D_
    if(content.hasClass('closed')){_x000D_
        _x000D_
        // disable transitions & set max-height to content height_x000D_
        content.removeClass('transitions').css('max-height', content.contentHeight);_x000D_
        setTimeout(function(){_x000D_
            _x000D_
            // enable & start transition_x000D_
            content.addClass('transitions').css({_x000D_
                'max-height': 0,_x000D_
                'opacity': 0_x000D_
            });_x000D_
            _x000D_
        }, 10); // 10ms timeout is the secret ingredient for disabling/enabling transitions_x000D_
        // chrome only needs 1ms but FF needs ~10ms or it chokes on the first animation for some reason_x000D_
        _x000D_
    }else if(content.hasClass('open')){  _x000D_
        _x000D_
        content.contentHeight += content.inner.outerHeight(); // if closed, add inner height to content height_x000D_
        content.css({_x000D_
            'max-height': content.contentHeight,_x000D_
            'opacity': 1_x000D_
        });_x000D_
        _x000D_
    }_x000D_
});
_x000D_
.transitions {_x000D_
    transition: all 0.5s ease-in-out;_x000D_
    -webkit-transition: all 0.5s ease-in-out;_x000D_
    -moz-transition: all 0.5s ease-in-out;_x000D_
}_x000D_
_x000D_
body {_x000D_
    font-family:Arial;_x000D_
    line-height: 3ex;_x000D_
}_x000D_
code {_x000D_
    display: inline-block;_x000D_
    background: #fafafa;_x000D_
    padding: 0 1ex;_x000D_
}_x000D_
#toggle {_x000D_
    display:block;_x000D_
    padding:10px;_x000D_
    margin:10px auto;_x000D_
    text-align:center;_x000D_
    width:30ex;_x000D_
}_x000D_
#content {_x000D_
    overflow:hidden;_x000D_
    margin:10px;_x000D_
    border:1px solid #666;_x000D_
    background:#efefef;_x000D_
    opacity:1;_x000D_
}_x000D_
#content .inner {_x000D_
    padding:10px;_x000D_
    overflow:auto;_x000D_
}
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>_x000D_
<div id="content" class="open">_x000D_
    <div class="inner">_x000D_
        <h3>Smooth CSS Transitions Between <code>height: 0</code> and <code>height: auto</code></h3>_x000D_
        <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>_x000D_
        <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>_x000D_
        <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>_x000D_
    </div>_x000D_
</div>_x000D_
_x000D_
<br />_x000D_
_x000D_
<button id="toggle">Challenge Accepted!</button>
_x000D_
_x000D_
_x000D_


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.

Use max-height with different transition easing and delay for each state.

HTML:

<a href="#" id="trigger">Hover</a>
<ul id="toggled">
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
<ul>

CSS:

#toggled{
    max-height: 0px;
    transition: max-height .8s cubic-bezier(0, 1, 0, 1) -.1s;
}

#trigger:hover + #toggled{
    max-height: 9999px;
    transition-timing-function: cubic-bezier(0.5, 0, 1, 0); 
    transition-delay: 0s;
}

See example: http://jsfiddle.net/0hnjehjc/1/


There seems to be no proper solution. max-height approach is quite good but doesn't work well for the hide phase - there will be a noticeable delay unless you know the height of the content.

I think the best way is to use max-height but only for the show phase. And not to use any animation on hiding. For most cases it shouldn't be crucial.

max-height should be set to a quite huge value to ensure any content fits. Animation speed can be controlled using transition duration (speed = max-height / duration). Speed does not depend on the size of the content. The time it takes to show the whole content will depend on its size.

_x000D_
_x000D_
document.querySelector("button").addEventListener(_x000D_
  "click", _x000D_
  function(){_x000D_
    document.querySelector("div").classList.toggle("hide");_x000D_
  }_x000D_
)
_x000D_
div {    _x000D_
    max-height: 20000px;_x000D_
    transition: max-height 3000ms;_x000D_
    overflow-y: hidden;_x000D_
}_x000D_
_x000D_
.hide {_x000D_
    max-height: 0;_x000D_
    transition: none;_x000D_
}
_x000D_
<button>Toggle</button>_x000D_
<div class="hide">Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. Lorem ipsum dolor sit amet, ius solet dignissim honestatis ad. Mea quem tibique intellegat te. Insolens deterruisset cum ea. Te omnes percipit consulatu eos. Vix novum primis salutatus no, eam denique sensibus et, his ipsum senserit ne. _x000D_
</div>
_x000D_
_x000D_
_x000D_


If you're using React, I can recommend react-animate-height:

<AnimateHeight height={isOpen ? 'auto' : 0}>
  // your content goes here
</AnimateHeight>

I know this is the thirty-somethingth answer to this question, but I think it's worth it, so here goes. This is a CSS-only solution with the following properties:

  • There is no delay at the beginning, and the transition doesn't stop early. In both directions (expanding and collapsing), if you specify a transition duration of 300ms in your CSS, then the transition takes 300ms, period.
  • It's transitioning the actual height (unlike transform: scaleY(0)), so it does the right thing if there's content after the collapsible element.
  • While (like in other solutions) there are magic numbers (like "pick a length that is higher than your box is ever going to be"), it's not fatal if your assumption ends up being wrong. The transition may not look amazing in that case, but before and after the transition, this is not a problem: In the expanded (height: auto) state, the whole content always has the correct height (unlike e.g. if you pick a max-height that turns out to be too low). And in the collapsed state, the height is zero as it should.

Demo

Here's a demo with three collapsible elements, all of different heights, that all use the same CSS. You might want to click "full page" after clicking "run snippet". Note that the JavaScript only toggles the collapsed CSS class, there's no measuring involved. (You could do this exact demo without any JavaScript at all by using a checkbox or :target). Also note that the part of the CSS that's responsible for the transition is pretty short, and the HTML only requires a single additional wrapper element.

_x000D_
_x000D_
$(function () {_x000D_
  $(".toggler").click(function () {_x000D_
    $(this).next().toggleClass("collapsed");_x000D_
    $(this).toggleClass("toggled"); // this just rotates the expander arrow_x000D_
  });_x000D_
});
_x000D_
.collapsible-wrapper {_x000D_
  display: flex;_x000D_
  overflow: hidden;_x000D_
}_x000D_
.collapsible-wrapper:after {_x000D_
  content: '';_x000D_
  height: 50px;_x000D_
  transition: height 0.3s linear, max-height 0s 0.3s linear;_x000D_
  max-height: 0px;_x000D_
}_x000D_
.collapsible {_x000D_
  transition: margin-bottom 0.3s cubic-bezier(0, 0, 0, 1);_x000D_
  margin-bottom: 0;_x000D_
  max-height: 1000000px;_x000D_
}_x000D_
.collapsible-wrapper.collapsed > .collapsible {_x000D_
  margin-bottom: -2000px;_x000D_
  transition: margin-bottom 0.3s cubic-bezier(1, 0, 1, 1),_x000D_
              visibility 0s 0.3s, max-height 0s 0.3s;_x000D_
  visibility: hidden;_x000D_
  max-height: 0;_x000D_
}_x000D_
.collapsible-wrapper.collapsed:after_x000D_
{_x000D_
  height: 0;_x000D_
  transition: height 0.3s linear;_x000D_
  max-height: 50px;_x000D_
}_x000D_
_x000D_
/* END of the collapsible implementation; the stuff below_x000D_
   is just styling for this demo */_x000D_
_x000D_
#container {_x000D_
  display: flex;_x000D_
  align-items: flex-start;_x000D_
  max-width: 1000px;_x000D_
  margin: 0 auto;_x000D_
}  _x000D_
_x000D_
_x000D_
.menu {_x000D_
  border: 1px solid #ccc;_x000D_
  box-shadow: 0 1px 3px rgba(0,0,0,0.5);_x000D_
  margin: 20px;_x000D_
_x000D_
  _x000D_
}_x000D_
_x000D_
.menu-item {_x000D_
  display: block;_x000D_
  background: linear-gradient(to bottom, #fff 0%,#eee 100%);_x000D_
  margin: 0;_x000D_
  padding: 1em;_x000D_
  line-height: 1.3;_x000D_
}_x000D_
.collapsible .menu-item {_x000D_
  border-left: 2px solid #888;_x000D_
  border-right: 2px solid #888;_x000D_
  background: linear-gradient(to bottom, #eee 0%,#ddd 100%);_x000D_
}_x000D_
.menu-item.toggler {_x000D_
  background: linear-gradient(to bottom, #aaa 0%,#888 100%);_x000D_
  color: white;_x000D_
  cursor: pointer;_x000D_
}_x000D_
.menu-item.toggler:before {_x000D_
  content: '';_x000D_
  display: block;_x000D_
  border-left: 8px solid white;_x000D_
  border-top: 8px solid transparent;_x000D_
  border-bottom: 8px solid transparent;_x000D_
  width: 0;_x000D_
  height: 0;_x000D_
  float: right;_x000D_
  transition: transform 0.3s ease-out;_x000D_
}_x000D_
.menu-item.toggler.toggled:before {_x000D_
  transform: rotate(90deg);_x000D_
}_x000D_
_x000D_
body { font-family: sans-serif; font-size: 14px; }_x000D_
_x000D_
*, *:after {_x000D_
  box-sizing: border-box;_x000D_
}
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>_x000D_
_x000D_
<div id="container">_x000D_
  <div class="menu">_x000D_
    <div class="menu-item">Something involving a holodeck</div>_x000D_
    <div class="menu-item">Send an away team</div>_x000D_
    <div class="menu-item toggler">Advanced solutions</div>_x000D_
    <div class="collapsible-wrapper collapsed">_x000D_
      <div class="collapsible">_x000D_
        <div class="menu-item">Separate saucer</div>_x000D_
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>_x000D_
        <div class="menu-item">Ask Worf</div>_x000D_
        <div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>_x000D_
        <div class="menu-item">Ask Q for help</div>_x000D_
      </div>_x000D_
    </div>_x000D_
    <div class="menu-item">Sweet-talk the alien aggressor</div>_x000D_
    <div class="menu-item">Re-route power from auxiliary systems</div>_x000D_
  </div>_x000D_
_x000D_
  <div class="menu">_x000D_
    <div class="menu-item">Something involving a holodeck</div>_x000D_
    <div class="menu-item">Send an away team</div>_x000D_
    <div class="menu-item toggler">Advanced solutions</div>_x000D_
    <div class="collapsible-wrapper collapsed">_x000D_
      <div class="collapsible">_x000D_
        <div class="menu-item">Separate saucer</div>_x000D_
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>_x000D_
      </div>_x000D_
    </div>_x000D_
    <div class="menu-item">Sweet-talk the alien aggressor</div>_x000D_
    <div class="menu-item">Re-route power from auxiliary systems</div>_x000D_
  </div>_x000D_
_x000D_
  <div class="menu">_x000D_
    <div class="menu-item">Something involving a holodeck</div>_x000D_
    <div class="menu-item">Send an away team</div>_x000D_
    <div class="menu-item toggler">Advanced solutions</div>_x000D_
    <div class="collapsible-wrapper collapsed">_x000D_
      <div class="collapsible">_x000D_
        <div class="menu-item">Separate saucer</div>_x000D_
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>_x000D_
        <div class="menu-item">Ask Worf</div>_x000D_
        <div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>_x000D_
        <div class="menu-item">Ask Q for help</div>_x000D_
        <div class="menu-item">Separate saucer</div>_x000D_
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>_x000D_
        <div class="menu-item">Ask Worf</div>_x000D_
        <div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>_x000D_
        <div class="menu-item">Ask Q for help</div>_x000D_
      </div>_x000D_
    </div>_x000D_
    <div class="menu-item">Sweet-talk the alien aggressor</div>_x000D_
    <div class="menu-item">Re-route power from auxiliary systems</div>_x000D_
  </div>_x000D_
_x000D_
</div>
_x000D_
_x000D_
_x000D_

How does it work?

There are in fact two transitions involved in making this happen. One of them transitions the margin-bottom from 0px (in the expanded state) to -2000px in the collapsed state (similar to this answer). The 2000 here is the first magic number, it's based on the assumption that your box won't be higher than this (2000 pixels seems like a reasonable choice).

Using the margin-bottom transition alone by itself has two issues:

  • If you actually have a box that's higher than 2000 pixels, then a margin-bottom: -2000px won't hide everything -- there'll be visible stuff even in the collapsed case. This is a minor fix that we'll do later.
  • If the actual box is, say, 1000 pixels high, and your transition is 300ms long, then the visible transition is already over after about 150ms (or, in the opposite direction, starts 150ms late).

Fixing this second issue is where the second transition comes in, and this transition conceptually targets the wrapper's minimum height ("conceptually" because we're not actually using the min-height property for this; more on that later).

Here's an animation that shows how combining the bottom margin transition with the minimum height transition, both of equal duration, gives us a combined transition from full height to zero height that has the same duration.

animation as described above

The left bar shows how the negative bottom margin pushes the bottom upwards, reducing the visible height. The middle bar shows how the minimum height ensures that in the collapsing case, the transition doesn't end early, and in the expanding case, the transition doesn't start late. The right bar shows how the combination of the two causes the box to transition from full height to zero height in the correct amount of time.

For my demo I've settled on 50px as the upper minimum height value. This is the second magic number, and it should be lower than the box' height would ever be. 50px seems reasonable as well; it seems unlikely that you'd very often want to make an element collapsible that isn't even 50 pixels high in the first place.

As you can see in the animation, the resulting transition is continuous, but it is not differentiable -- at the moment when the minimum height is equal to the full height adjusted by the bottom margin, there is a sudden change in speed. This is very noticeable in the animation because it uses a linear timing function for both transitions, and because the whole transition is very slow. In the actual case (my demo at the top), the transition only takes 300ms, and the bottom margin transition is not linear. I've played around with a lot of different timing functions for both transitions, and the ones I ended up with felt like they worked best for the widest variety of cases.

Two problems remain to fix:

  1. the point from above, where boxes of more than 2000 pixels height aren't completely hidden in the collapsed state,
  2. and the reverse problem, where in the non-hidden case, boxes of less than 50 pixels height are too high even when the transition isn't running, because the minimum height keeps them at 50 pixels.

We solve the first problem by giving the container element a max-height: 0 in the collapsed case, with a 0s 0.3s transition. This means that it's not really a transition, but the max-height is applied with a delay; it only applies once the transition is over. For this to work correctly, we also need to pick a numerical max-height for the opposite, non-collapsed, state. But unlike in the 2000px case, where picking too large of a number affects the quality of the transition, in this case, it really doesn't matter. So we can just pick a number that is so high that we know that no height will ever come close to this. I picked a million pixels. If you feel you may need to support content of a height of more than a million pixels, then 1) I'm sorry, and 2) just add a couple of zeros.

The second problem is the reason why we're not actually using min-height for the minimum height transition. Instead, there is an ::after pseudo-element in the container with a height that transitions from 50px to zero. This has the same effect as a min-height: It won't let the container shrink below whatever height the pseudo-element currently has. But because we're using height, not min-height, we can now use max-height (once again applied with a delay) to set the pseudo-element's actual height to zero once the transition is over, ensuring that at least outside the transition, even small elements have the correct height. Because min-height is stronger than max-height, this wouldn't work if we used the container's min-height instead of the pseudo-element's height. Just like the max-height in the previous paragraph, this max-height also needs a value for the opposite end of the transition. But in this case we can just pick the 50px.

Tested in Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (except for a flexbox layout issue with my demo that I didn't bother debugging), and Safari (Mac, iOS). Speaking of flexbox, it should be possible to make this work without using any flexbox; in fact I think you could make almost everything work in IE7 – except for the fact that you won't have CSS transitions, making it a rather pointless exercise.


There was little mention of the Element.prototype.scrollHeight property which can be useful here and still may be used with a pure CSS transition, although scripting support would obviously be required. The property always contains the "full" height of an element, regardless of whether and how its content overflows as a result of collapsed height (e.g. height: 0).

As such, for a height: 0 (effectively fully collapsed) element, its "normal" or "full" height is still readily available through its scrollHeight value (invariably a pixel length).

For such an element, assuming it already has the transition set up like e.g. (using ul as per original question):

ul {
    height: 0;
    transition: height 1s; /* An example transition. */
}

We can trigger desired animated "expansion" of height, using CSS only, with something like the following (here assuming ul variable refers to the list):

ul.style.height = ul.scrollHeight + "px";

That's it. If you need to collapse the list, either of the two following statements will do:

ul.style.height = "0";
ul.style.removeProperty("height");

My particular use case revolved around animating lists of unknown and often considerable lengths, so I was not comfortable settling on an arbitrary "large enough" height or max-height specification and risking cut-off content or content that you suddenly need to scroll (if overflow: auto, for example). Additionally, the easing and timing is broken with max-height-based solutions, because the used height may reach its maximum value a lot sooner than it would take for max-height to reach 9999px. And as screen resolutions grow, pixel lengths like 9999px leave a bad taste in my mouth. This particular solution solves the problem in an elegant manner, in my opinion.

Finally, here is hoping that future revisions of CSS address authors' need to do these kind of things even more elegantly -- revisit the notion of "computed" vs "used" and "resolved" values, and consider whether transitions should apply to computed values, including transitions with width and height (which currently get a bit of a special treatment).


The solution that I've always used was to first fade out, then shrink the font-size, padding and margin values. It doesn't look the same as a wipe, but it works without a static height or max-height.

Working example:

_x000D_
_x000D_
/* final display */_x000D_
#menu #list {_x000D_
    margin: .5em 1em;_x000D_
    padding: 1em;_x000D_
}_x000D_
_x000D_
/* hide */_x000D_
#menu:not(:hover) #list {_x000D_
    font-size: 0;_x000D_
    margin: 0;_x000D_
    opacity: 0;_x000D_
    padding: 0;_x000D_
    /* fade out, then shrink */_x000D_
    transition: opacity .25s,_x000D_
                font-size .5s .25s,_x000D_
                margin .5s .25s,_x000D_
                padding .5s .25s;_x000D_
}_x000D_
_x000D_
/* reveal */_x000D_
#menu:hover #list {_x000D_
    /* unshrink, then fade in */_x000D_
    transition: font-size .25s,_x000D_
                margin .25s,_x000D_
                padding .25s,_x000D_
                opacity .5s .25s;_x000D_
}
_x000D_
<div id="menu">_x000D_
    <b>hover me</b>_x000D_
    <ul id="list">_x000D_
        <li>item</li>_x000D_
        <li>item</li>_x000D_
        <li>item</li>_x000D_
        <li>item</li>_x000D_
        <li>item</li>_x000D_
    </ul>_x000D_
</div>_x000D_
_x000D_
<p>Another paragraph...</p>
_x000D_
_x000D_
_x000D_


No hard coded values.

No JavaScript.

No approximations.

The trick is to use a hidden & duplicated div to get the browser to understand what 100% means.

This method is suitable whenever you're able to duplicate the DOM of the element you wish to animate.

_x000D_
_x000D_
.outer {_x000D_
  border: dashed red 1px;_x000D_
  position: relative;_x000D_
}_x000D_
_x000D_
.dummy {_x000D_
  visibility: hidden;_x000D_
}_x000D_
_x000D_
.real {_x000D_
  position: absolute;_x000D_
  background: yellow;_x000D_
  height: 0;_x000D_
  transition: height 0.5s;_x000D_
  overflow: hidden;_x000D_
}_x000D_
_x000D_
.outer:hover>.real {_x000D_
  height: 100%;_x000D_
}
_x000D_
Hover over the box below:_x000D_
<div class="outer">_x000D_
  <!-- The actual element that you'd like to animate -->_x000D_
  <div class="real">_x000D_
unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable_x000D_
content unpredictable content unpredictable content unpredictable content_x000D_
  </div>_x000D_
  <!-- An exact copy of the element you'd like to animate. -->_x000D_
  <div class="dummy" aria-hidden="true">_x000D_
unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable_x000D_
content unpredictable content unpredictable content unpredictable content_x000D_
  </div>_x000D_
</div>
_x000D_
_x000D_
_x000D_


This is regular problem I've solved like this

http://jsfiddle.net/ipeshev/d1dfr0jz/

Try to set delay of closed state to some negative number and play a little bit with the value. You will see the difference.It can be made almost to lie the human eye ;).

It works in major browsers, but good enough for me. It is strange but give some results.

.expandable {
    max-height: 0px;
    overflow: hidden;
    transition: all 1s linear -0.8s;
}

button:hover ~ .expandable {
    max-height: 9000px;
    transition: all 1s ease-in-out;
}

Expanding on @jake's answer, the transition will go all the way to the max height value, causing an extremely fast animation - if you set the transitions for both :hover and off you can then control the crazy speed a little bit more.

So the li:hover is when the mouse enters the state and then the transition on the non-hovered property will be the mouse leave.

Hopefully this will be of some help.

e.g:

.sidemenu li ul {
   max-height: 0px;
   -webkit-transition: all .3s ease;
   -moz-transition: all .3s ease;
   -o-transition: all .3s ease;
   -ms-transition: all .3s ease;
   transition: all .3s ease;
}
.sidemenu li:hover ul {
    max-height: 500px;
    -webkit-transition: all 1s ease;
   -moz-transition: all 1s ease;
   -o-transition: all 1s ease;
   -ms-transition: all 1s ease;
   transition: all 1s ease;
}
/* Adjust speeds to the possible height of the list */

Here's a fiddle: http://jsfiddle.net/BukwJ/


You can, with a little bit of non-semantic jiggery-pokery. My usual approach is to animate the height of an outer DIV which has a single child which is a style-less DIV used only for measuring the content height.

_x000D_
_x000D_
function growDiv() {_x000D_
  var growDiv = document.getElementById('grow');_x000D_
  if (growDiv.clientHeight) {_x000D_
    growDiv.style.height = 0;_x000D_
  } else {_x000D_
    var wrapper = document.querySelector('.measuringWrapper');_x000D_
    growDiv.style.height = wrapper.clientHeight + "px";_x000D_
  }_x000D_
}
_x000D_
#grow {_x000D_
  -moz-transition: height .5s;_x000D_
  -ms-transition: height .5s;_x000D_
  -o-transition: height .5s;_x000D_
  -webkit-transition: height .5s;_x000D_
  transition: height .5s;_x000D_
  height: 0;_x000D_
  overflow: hidden;_x000D_
  outline: 1px solid red;_x000D_
}
_x000D_
<input type="button" onclick="growDiv()" value="grow">_x000D_
<div id='grow'>_x000D_
  <div class='measuringWrapper'>_x000D_
    <div>_x000D_
      The contents of my div._x000D_
    </div>_x000D_
    <div>_x000D_
      The contents of my div._x000D_
    </div>_x000D_
    <div>_x000D_
      The contents of my div._x000D_
    </div>_x000D_
    <div>_x000D_
      The contents of my div._x000D_
    </div>_x000D_
    <div>_x000D_
      The contents of my div._x000D_
    </div>_x000D_
    <div>_x000D_
      The contents of my div._x000D_
    </div>_x000D_
  </div>_x000D_
</div>
_x000D_
_x000D_
_x000D_

One would like to just be able to dispense with the .measuringWrapper and just set the DIV's height to auto and have that animate, but that doesn't seem to work (the height gets set, but no animation occurs).

_x000D_
_x000D_
function growDiv() {_x000D_
  var growDiv = document.getElementById('grow');_x000D_
  if (growDiv.clientHeight) {_x000D_
    growDiv.style.height = 0;_x000D_
  } else {_x000D_
    growDiv.style.height = 'auto';_x000D_
  }_x000D_
}
_x000D_
#grow {_x000D_
  -moz-transition: height .5s;_x000D_
  -ms-transition: height .5s;_x000D_
  -o-transition: height .5s;_x000D_
  -webkit-transition: height .5s;_x000D_
  transition: height .5s;_x000D_
  height: 0;_x000D_
  overflow: hidden;_x000D_
  outline: 1px solid red;_x000D_
}
_x000D_
<input type="button" onclick="growDiv()" value="grow">_x000D_
<div id='grow'>_x000D_
  <div>_x000D_
    The contents of my div._x000D_
  </div>_x000D_
  <div>_x000D_
    The contents of my div._x000D_
  </div>_x000D_
  <div>_x000D_
    The contents of my div._x000D_
  </div>_x000D_
  <div>_x000D_
    The contents of my div._x000D_
  </div>_x000D_
  <div>_x000D_
    The contents of my div._x000D_
  </div>_x000D_
  <div>_x000D_
    The contents of my div._x000D_
  </div>_x000D_
</div>
_x000D_
_x000D_
_x000D_

My interpretation is that an explicit height is needed for the animation to run. You can't get an animation on height when either height (the start or end height) is auto.


I have not read everything in detail but I have had this problem recently and I did what follows:

div.class{
   min-height:1%;
   max-height:200px;
   -webkit-transition: all 0.5s ease;
   -moz-transition: all 0.5s ease;
   -o-transition: all 0.5s ease;
   -webkit-transition: all 0.5s ease;
   transition: all 0.5s ease;
   overflow:hidden;
}

div.class:hover{
   min-height:100%;
   max-height:3000px;
}

This allows you to have a div that at first shows content up to 200px height and on hover it's size becomes at least as high as the whole content of the div. The Div does not become 3000px but 3000px is the limit that I am imposing. Make sure to have the transition on the non :hover, otherwise you might get some strange rendering. In this way the :hover inherits from the non :hover.

Transition does not work form px to % or to auto. You need to use same unit of measure. This works fine for me. Using HTML5 makes it perfect....

Remember that there is always a work around... ; )

Hope someone finds this useful


I think I came up with a really solid solution

OK! I know this problem is as old as the internet but I think I have a solution which I turned into a plugin called mutant-transition. My solution sets the style="" attributes for tracked elements whenever theres a change in the DOM. the end result is that you can use good ole CSS for your transitions and not use hacky fixes or special javascript. The only thing you have to do is set what you want to track on the element in question using data-mutant-attributes="X".

<div data-mutant-attributes="height">                                                                      
        This is an example with mutant-transition                                                                                                          
    </div>

Thats it! This solution uses MutationObserver to follow changes in the DOM. Because of this, you don't really have to set anything up or use javascript to manually animate things. Changes are tracked automatically. However, because it uses MutationObserver, this will only transition in IE11+.

Fiddles!