By Hailwood


2010-08-18 02:50:35 8 Comments

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

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

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

How else could I do this without using JavaScript?

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

30 comments

@scripter 2019-06-14 20:34:06

For a purely CSS solution that animates max-height that doesn't have a massive delay, I would consider setting a reasonable max-height on hover, somewhere around 500px or roughly around or slightly larger that the height of most of the elements that you want to animate, and have larger contents scroll by setting overflow-y to auto after the animation is complete using a 0.5s delay.

Then set your transition time to something around 0.3s (or slightly slower if the expansion will end up moving other content around on your page) with an exponential ease-out cubic-bezier curve on open, and an ease-in cubic-bezier curve and somewhat quicker on close, such as 0.15s, as when people dismiss something from the page, they typically don't really want wait around to watch it go away.

These quick animations will be still visible to the user, and minimize the effect of any max-height delay causing your page to feel sluggish.

The code would look something like this:

#child0 {
  max-height: 0;
  overflow-y: hidden;
  background-color: #dedede;
  -webkit-transition: max-height 0.15s cubic-bezier(0.7, 0, 1, 0.5), overflow-y 0s linear 0s;
  -moz-transition: max-height 0.15s cubic-bezier(0.7, 0, 1, 0.5), overflow-y 0s linear 0s;
  -o-transition: max-height 0.15s cubic-bezier(0.7, 0, 1, 0.5), overflow-y 0s linear 0s;
  transition: max-height 0.15s cubic-bezier(0.7, 0, 1, 0.5), overflow-y 0s linear 0s;
}
#parent0:hover #child0 {
  max-height: 500px;
  overflow-y: auto;
  -webkit-transition: max-height 0.3s cubic-bezier(0.1, 0.9, 0.2, 1), overflow-y 0s linear 0.3s;
  -moz-transition: max-height 0.3s cubic-bezier(0.1, 0.9, 0.2, 1), overflow-y 0s linear 0.3s;
  -o-transition: max-height 0.3s cubic-bezier(0.1, 0.9, 0.2, 1), overflow-y 0s linear 0.3s;
  transition: max-height 0.3s cubic-bezier(0.1, 0.9, 0.2, 1), overflow-y 0s linear 0.3s;
}

@shunryu111 2019-02-15 11:51:27

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

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

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

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

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

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

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

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

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

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

@Adam Taylor 2019-05-17 04:13:59

+1 This answer worked pretty well for me. However, I found that giving the setTimeout no delay would sometimes cause the transition to not happen (the element would go to zero height instantly), presumably because the DOM hadn't been updated with the fixed height. Adding a delay of even something pretty small like 50 fixed it for me.

@Ardhi 2019-02-09 08:34:37

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

@Sijav 2013-10-06 10:34:14

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.

@Steven Vachon 2015-05-29 14:05:06

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:

/* final display */
#menu #list {
    margin: .5em 1em;
    padding: 1em;
}

/* hide */
#menu:not(:hover) #list {
    font-size: 0;
    margin: 0;
    opacity: 0;
    padding: 0;
    /* fade out, then shrink */
    transition: opacity .25s,
                font-size .5s .25s,
                margin .5s .25s,
                padding .5s .25s;
}

/* reveal */
#menu:hover #list {
    /* unshrink, then fade in */
    transition: font-size .25s,
                margin .25s,
                padding .25s,
                opacity .5s .25s;
}
<div id="menu">
    <b>hover me</b>
    <ul id="list">
        <li>item</li>
        <li>item</li>
        <li>item</li>
        <li>item</li>
        <li>item</li>
    </ul>
</div>

<p>Another paragraph...</p>

@Cruz Nunez 2016-07-01 04:12:14

Worked neatly for me. Not quite like the jQuery slideUp/Down. You can see a difference only with weak CPU's, like old computers or mobile, and opening and closing many of them at the same time.

@Dennis W 2016-10-23 22:30:48

If you have buttons or any other non-text elements you will end up with unwanted whitespace while not hovering.

@Steven Vachon 2016-10-24 21:02:55

You can further refine it by selecting all child elements with * and applying other changes. Buttons should've been affected by my above code, however, if they were correctly styled with em.

@Andrey Shatilov 2017-10-14 11:18:12

In my case, I also needed to add line-height: 0; and border-width: 0;

@Steven Vachon 2017-10-18 13:13:55

You shouldn't need to adjust line-height if you correctly avoid units on the value: developer.mozilla.org/en-US/docs/Web/CSS/…

@jake 2011-11-30 18:42:56

Use max-height in the transformation and not height. And set a value on max-height to something bigger than your box will ever get.

See JSFiddle demo provided by Chris Jordan in another answer here.

#menu #list {
    max-height: 0;
    transition: max-height 0.15s ease-out;
    overflow: hidden;
    background: #d5d5d5;
}

#menu:hover #list {
    max-height: 500px;
    transition: max-height 0.25s ease-in;
}
<div id="menu">
    <a>hover me</a>
    <ul id="list">
        <!-- Create a bunch, or not a bunch, of li's to see the timing. -->
        <li>item</li>
        <li>item</li>
        <li>item</li>
        <li>item</li>
        <li>item</li>
    </ul>
</div>

@vsync 2011-12-05 16:03:52

this works great! except there is a delay when it starts, because it starts for max-height which initially is very high..hmm, i think this is somewhat annoying

@kingjeffrey 2012-03-03 04:15:59

+1 Great solution! The speed of the transition is calculated is calculated as the time you specify to transition to the max-height value... but since height will be less than max-height, the transition to actual height will occur faster (often significantly) than the time specified.

@Daniel Imms 2012-11-20 04:16:43

The transition occurring at a slower rate as @kingjeffrey mentions could also be a good thing. Say you're expanding a menu, the menu will expand at a constant rate for each menu item, so expanding a menu with 9 items will take 9 times longer than a menu with 1. Regular behaviour when using height would the menu with 1 item expand significantly slower than the one with 9. May be desirable depending on the situation.

@jpeltoniemi 2013-05-17 10:22:01

Note that this may cause ugly transition ending when you have to use values that are much bigger than the actual computed value. I noticed this while trying to make a div grow from 0 height to the content height that varies greatly due to different screen sizes(2 lines on my 2560x1440 monitor vs >10 lines on a smartphone). For this I ended up going with js.

@Pluto 2013-06-04 15:54:31

It took me long enough to figure out why the transitions were so different... I guess next time I should've been reading the comments more. So yeah the transitions will happen much faster than expected, though this can be managed by changing the types of transitions used. Use ease-out when reducing the max-height to 0, and ease-in when increasing the max-size.

@Tim 2013-11-20 14:57:32

Very ugly solution since it creates a delay in one direction but not the other.

@Loenix 2014-02-17 16:47:12

With this solution the duration of the effect is not respected. This is very annoying with complete callback.

@Shawn Whinnery 2014-04-16 22:06:02

This is a pretty lazy solution. I'm assuming OP wants to use height : auto because the expanded height of the container is somewhat unpredictable. This solution will cause a delay before the animation becomes visible. Additionally the visible duration of the animation will be unpredictable. You'll get much more predictable (and likely smoother) results by calculating the combined height of each of the containers child nodes and then easing to an exact height value.

@sb. 2014-09-15 13:10:11

I think it's worth noting that the final max-height value must not be too big - at least in Chrome, it won't work then

@Samuel Ramzan 2014-11-28 19:23:17

You can add "all" like: { transition all 1s ease }

@Izkata 2014-12-17 21:47:43

@TomášZato display:none breaks it, the second max-height is so large it expands at absurd speeds making it look instant, and I added overflow:hidden to keep the text from going outside of the container. So this version of your fiddle works

@GraphicsMuncher 2014-12-24 03:21:29

This breaks as soon as you have variable-length items. There's a delay when it slides up, and the amount of time it takes to transition is variable (depends on "high" value of max-height and size of actual element). The whole point is to simulate how auto works.

@Spyder 2015-01-15 23:56:44

By the way, in the JSFiddle it uses setTimeout to make the animation "stick". It turns out all that is required is a reflow (query offsetHeight).

@Vandervals 2015-05-29 09:11:07

Doesn't this create a false delay and speed impression?

@VIDesignz 2015-08-02 02:56:35

All these answers are not quite "right"....I have a solution (added answer today) with only two CSS declarations. It does not use max-height. It's pure CSS and super simple to set up. Works on relative positioning. Here is a fiddle jsfiddle.net/n5XfG/2596

@Dom Vinyard 2015-09-19 23:33:28

VIDesignz, significant delay before execution in Safari 9,

@Triynko 2015-11-12 19:49:15

I have to second Tim's comment: "Very ugly solution since it creates a delay in one direction but not the other." When opening a hidden div, the transition from 0 to height starts immediately when triggered. However, when hiding the div, the transition starts at max-height (not height), so there's no height-change visually while it transitions from max-height to height (during this time, other animations have already occurred, hence, it looks like the height change is delayed), and it finally has a visible height change as it goes from height to 0. Terrible. Terrible. Terrible.

@MaxRocket 2016-02-08 15:50:53

You can also use min-height, which is nice because that way the height can expand to longer content.

@Mattijs 2016-04-13 04:14:07

@Triynko Can you come up with solution instead of just repeating what other people already mentioned a year ago? Easy to complain, hard to come up with an alternative, right?

@Andy 2016-06-11 02:39:38

I've found a partial fix for the delay issue when using this technique with variable length content - by animating the opacity of the #list element, along with its max-height, the delay appears to be more natural, as the opacity begins to fade just before the content starts to slide up, and slowly fades back in as the content immediately starts sliding down. It's not perfect but I think it's much better than just animating the max-height by itself.

@Matt Kieran 2016-09-12 00:13:52

If applicable (as it was in my situation) you can apply the transition to the opened state, that way when you use max-height it will expand correctly (although the timing may need tweaking) and when you return the max-height to zero it will collapse it instantly. From a visual standpoint, it looks perfect and has no weird delay. For me this was a suitable solution.

@DavidTaubmann 2016-10-22 21:21:21

This solution has an error when used in :target, the old target closes AFTER the new one has been opened, this moves the content's scrolling position after it has been automatically located by the browsers hash activity, to avoid you can mix scaleY or transformY and Max-Height, check my comment on @Andrew Messier answer for a link to compare

@TylerH 2016-12-16 21:22:05

@daniel.betts Please do not change the code to another user's answer. If you have a separate solution that has not been provided as an answer already, please add it as its own answer, instead.

@Meliodas 2017-01-24 16:58:25

There's a delay when it starts if your max-height is too big. To solve that, use cubic-bezier(0,1,0,1) instead of ease-in and cubic-bezier(1,0,1,0) instead of ease-out

@BoltClock 2017-04-01 06:22:15

@Triynko: People just can't accept the fact that this is a fundamental limitation of CSS layout that cannot reliably be solved without hacks and workarounds all with their own trade-offs. It's why there are so many answers to this question, and why people settle for "solutions" that have already been demonstrated to be wrong or otherwise flawed. It's sad, really. I'd post an answer going in depth about why this is a fundamental limitation, but considering the sheer number of answers here already I'm not sure it's worth my time.

@George 2018-01-21 17:07:01

I don't like this solution. The transition time will not relate to other animations transition times.

@trainoasis 2018-07-11 07:26:41

This won't work if your initial element shouldn't have max-height: 0, which is the case when you are initially hiding a child element that in turn makes parent element bigger when you show it on hover:)

@gaurav5430 2019-03-07 16:39:34

Since we are setting max-height 0 and not actually hiding the elements, they would still be read by the screenreader, which may or may not be what you want.

@gaurav5430 2019-03-07 16:43:00

Also, they would still be in tab order in case they contain some tabbable elements.

@funky-nd 2019-04-09 15:55:51

i dont know why people do not like this solution. this is perfect.

@Sam 2019-04-26 20:08:00

@Meliodas Thanks for the cubic-bezier solution, this works well even if I set max-height to 50,000 (just to make sure my content doesn't get chopped off).

@Bug Whisperer 2019-04-30 20:31:20

is this still the only way to do it if the actual height of the element varies and is unknown?

@Shahid Kamal 2019-05-22 12:47:37

amazing worked great

@Bug Whisperer 2019-05-22 20:54:30

honestly you deserve a pot of gold for this answer even tho its kinda hack-ish

@amn 2016-07-05 18:59:40

There was little mention of the Element.scrollHeight property which can be useful here and still may be used with a pure CSS transition. 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).

@TylerH 2016-07-05 21:12:44

You didn't find it in the other answers here because interacting with the DOM using the Element.scrollHeight property requires JavaScript and, as the question clearly states, they want to do this without JavaScript.

@amn 2016-07-05 21:29:37

Right. When something is too good to be true, it usually is. I also found some mention of scrollHeight by doing a more thorough search this time, although the code using it seems rather convoluted and definitely more JavaScript than is necessary today. Anyway, I suppose you are right on point with the no-JavaScript, but without Javascript you are limited to :active and :focus and perhaps a couple other pseudo-classes, so I am wondering exactly what is meant by "No JavaScript". As in "pure CSS animation"? Because setting a style in JavaScript is still pure and degradable CSS animation.

@TylerH 2016-07-06 13:33:41

There are lots of other parts of CSS other than those two pseudo-classes that can solve this issue, as the highest-upvoted answers on page 1 show. Setting a style in JavaScript is not "pure CSS" since it requires JS to be set in the first place. Often when a person asks for a CSS-only solution, it's because they are not able to inject any JavaScript, or not allowed to by their current project or role.

@amn 2016-07-06 14:21:55

"Lots of other parts"? Which ones? :hover? That's one. And that's it. Good luck triggering transitions on touch or click events without JavaScript. That said, I never said I consider my answer correct, I am merely debating its usefulness amongst "hacks" using max-height: 9999px. I also am fully aware that JavaScript is not often viable or enabled, in fact, I am rather conservative about it myself and not one of those gung-ho full-stack React / Angular cowboys.

@amn 2016-07-06 15:39:33

You don't need to help me. Now, the most upvoted answer includes :hover in its style snippet, instrumental for the solution. The next one with a working solution does as well. Could you please link the answer you were referring to? Also, my answer works just as well with max-height as with height, it's of no significance for the solution and was never a debated point.

@Lee Comstock 2017-08-04 21:01:19

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.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  transition: 0.25s;
  font-family: monospace;
}

body {
  margin: 10px 0 0 10px;
}

.box {
  width: 150px;
  height: 150px;
  margin: 0 2px 10px 0;
  background: #2d333b;
  border: solid 10px #20262e;
  overflow: hidden;
  display: inline-flex;
  flex-direction: column;
}

.space {
  flex-basis: 100%;
  flex-grow: 1;
  flex-shrink: 0;    
}

p {
  flex-basis: auto;
  flex-grow: 0;
  flex-shrink: 1;
  background: #20262e;
  padding: 10px;
  width: 100%;
  text-align: left;
  color: white;
}

.box:hover .space {
  flex-grow: 0;
  flex-shrink: 1;
}
  
.box:hover p {
  flex-grow: 1;
  flex-shrink: 0;    
}
<div class="box">
  <div class="space"></div>
  <p>
    Super Metroid Prime Fusion
  </p>
</div>
<div class="box">
  <div class="space"></div>
  <p>
    Resident Evil 2 Remake
  </p>
</div>
<div class="box">
  <div class="space"></div>
  <p>
    Yolo The Game
  </p>
</div>
<div class="box">
  <div class="space"></div>
  <p>
    Final Fantasy 7 Remake + All Additional DLC + Golden Tophat
  </p>
</div>
<div class="box">
  <div class="space"></div>
  <p>
    DerpVille
  </p>
</div>

JS Fiddle

@jhurshman 2010-06-30 13:32:08

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.

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

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

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

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.

@Quickredfox 2013-04-29 21:23:46

Since this relies on javascript, you could also easily add the measuringWrapper using javascript too!

@HartleySan 2014-07-16 02:28:57

Quickredfox, I like your suggestion. I was able to use your suggestion in combination with the replaceChild method and the following technique to add the wrapper div without disrupting any existing DOM element references: stackoverflow.com/questions/6938248/…. Also, I didn't like how I couldn't reopen a closing div midstream, so I added an isOpening Boolean property to the DOM object, and then used that to tell whether the div was opening or closing. That gave me a lot more control for an accordion module I built out. Anyway, thanks again. Great answer!

@user1742529 2015-06-17 09:30:26

You can do it without wrapper. Just: function growDiv() { var growDiv = document.getElementById('grow'); if (growDiv.clientHeight) { growDiv.style.height = 0; } else { growDiv.style.height = growDiv.scrollHeight+'px'; } }

@VIDesignz 2015-08-02 03:03:46

Check this out...talk about a simple answer jsfiddle.net/n5XfG/2596 ...Your answer is insanely complex for no good reason. No need for max-height, no need for auto, and especially no need for Javascript!! Just two simple declarations

@Timo Türschmann 2016-04-15 08:39:43

@VIDesignz You animate over margin: 100%. This is 100% of the width of the element, not its height! This means, if you have an element with a height bigger than it's width, this will not hide it. Also, the actual animation speed is totally different than the one that is defined.

@Coderer 2017-06-23 11:16:59

I was excited about VIDesignz's answer until I read more about Timo's comment. Yes, margin-top (and margin-bottom) are relative to width when defined in percentages, yes that's stupid, but it also explains why the open and close animation timings are completely different -- it's the same thing you see in the top-rated answer, specifying a hard-coded max-height. Why is this so hard to do right?

@user3411192 2018-02-27 22:11:44

I made a jquery version of this: jsfiddle.net/ecupaio/s3zhosr6/11

@Vincent Hoch-Drei 2018-04-17 08:06:02

Wow, the only solution that really work! But... Please note that overflow: hidden is important. Mabye you could edit your awnser and add the note.

@Vivek Maharajh 2016-02-26 17:19:20

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.

.outer {
  border: dashed red 1px;
  position: relative;
}

.dummy {
  visibility: hidden;
}

.real {
  position: absolute;
  background: yellow;
  height: 0;
  transition: height 0.5s;
  overflow: hidden;
}

.outer:hover>.real {
  height: 100%;
}
Hover over the box below:
<div class="outer">
  <!-- The actual element that you'd like to animate -->
  <div class="real">
unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable
content unpredictable content unpredictable content unpredictable content
  </div>
  <!-- An exact copy of the element you'd like to animate. -->
  <div class="dummy" aria-hidden="true">
unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable content unpredictable
content unpredictable content unpredictable content unpredictable content
  </div>
</div>

@Karl Adler 2016-06-10 08:08:35

nice solution, but it really hurts to see the code duplication here...

@M_Willett 2016-06-13 13:13:44

Nicely done - this is a valid solution to a problem that really shouldn't exist. Animating max-height is bas unless the transition is set to "linear", it's also (obviously) very bad for animating CMS content where the height is variable.

@balpha 2017-05-14 14:33:47

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.

$(function () {
  $(".toggler").click(function () {
    $(this).next().toggleClass("collapsed");
    $(this).toggleClass("toggled"); // this just rotates the expander arrow
  });
});
.collapsible-wrapper {
  display: flex;
  overflow: hidden;
}
.collapsible-wrapper:after {
  content: '';
  height: 50px;
  transition: height 0.3s linear, max-height 0s 0.3s linear;
  max-height: 0px;
}
.collapsible {
  transition: margin-bottom 0.3s cubic-bezier(0, 0, 0, 1);
  margin-bottom: 0;
  max-height: 1000000px;
}
.collapsible-wrapper.collapsed > .collapsible {
  margin-bottom: -2000px;
  transition: margin-bottom 0.3s cubic-bezier(1, 0, 1, 1),
              visibility 0s 0.3s, max-height 0s 0.3s;
  visibility: hidden;
  max-height: 0;
}
.collapsible-wrapper.collapsed:after
{
  height: 0;
  transition: height 0.3s linear;
  max-height: 50px;
}

/* END of the collapsible implementation; the stuff below
   is just styling for this demo */

#container {
  display: flex;
  align-items: flex-start;
  max-width: 1000px;
  margin: 0 auto;
}  


.menu {
  border: 1px solid #ccc;
  box-shadow: 0 1px 3px rgba(0,0,0,0.5);
  margin: 20px;

  
}

.menu-item {
  display: block;
  background: linear-gradient(to bottom, #fff 0%,#eee 100%);
  margin: 0;
  padding: 1em;
  line-height: 1.3;
}
.collapsible .menu-item {
  border-left: 2px solid #888;
  border-right: 2px solid #888;
  background: linear-gradient(to bottom, #eee 0%,#ddd 100%);
}
.menu-item.toggler {
  background: linear-gradient(to bottom, #aaa 0%,#888 100%);
  color: white;
  cursor: pointer;
}
.menu-item.toggler:before {
  content: '';
  display: block;
  border-left: 8px solid white;
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  width: 0;
  height: 0;
  float: right;
  transition: transform 0.3s ease-out;
}
.menu-item.toggler.toggled:before {
  transform: rotate(90deg);
}

body { font-family: sans-serif; font-size: 14px; }

*, *:after {
  box-sizing: border-box;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="container">
  <div class="menu">
    <div class="menu-item">Something involving a holodeck</div>
    <div class="menu-item">Send an away team</div>
    <div class="menu-item toggler">Advanced solutions</div>
    <div class="collapsible-wrapper collapsed">
      <div class="collapsible">
        <div class="menu-item">Separate saucer</div>
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
        <div class="menu-item">Ask Worf</div>
        <div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
        <div class="menu-item">Ask Q for help</div>
      </div>
    </div>
    <div class="menu-item">Sweet-talk the alien aggressor</div>
    <div class="menu-item">Re-route power from auxiliary systems</div>
  </div>

  <div class="menu">
    <div class="menu-item">Something involving a holodeck</div>
    <div class="menu-item">Send an away team</div>
    <div class="menu-item toggler">Advanced solutions</div>
    <div class="collapsible-wrapper collapsed">
      <div class="collapsible">
        <div class="menu-item">Separate saucer</div>
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
      </div>
    </div>
    <div class="menu-item">Sweet-talk the alien aggressor</div>
    <div class="menu-item">Re-route power from auxiliary systems</div>
  </div>

  <div class="menu">
    <div class="menu-item">Something involving a holodeck</div>
    <div class="menu-item">Send an away team</div>
    <div class="menu-item toggler">Advanced solutions</div>
    <div class="collapsible-wrapper collapsed">
      <div class="collapsible">
        <div class="menu-item">Separate saucer</div>
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
        <div class="menu-item">Ask Worf</div>
        <div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
        <div class="menu-item">Ask Q for help</div>
        <div class="menu-item">Separate saucer</div>
        <div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
        <div class="menu-item">Ask Worf</div>
        <div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
        <div class="menu-item">Ask Q for help</div>
      </div>
    </div>
    <div class="menu-item">Sweet-talk the alien aggressor</div>
    <div class="menu-item">Re-route power from auxiliary systems</div>
  </div>

</div>

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.

@Gil Epshtain 2018-07-30 15:16:03

I wish you could shorten (simplify) your solution, or the code example at least - it looks like 90% of the code example isn't relevant to your answer.

@Andrej 2017-03-24 19:38:47

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.

document.querySelector("button").addEventListener(
  "click", 
  function(){
    document.querySelector("div").classList.toggle("hide");
  }
)
div {    
    max-height: 20000px;
    transition: max-height 3000ms;
    overflow-y: hidden;
}

.hide {
    max-height: 0;
    transition: none;
}
<button>Toggle</button>
<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. 
</div>

@Alexander V. Ulyanov 2015-11-23 08:16:22

If quantity of "li" > 30 pieces then height of wrap will > 500px. My answer:

ul{width:100%}
li{height:0;overflow:hidden;background:#dedede;transition:.2s.4s linear}
ul:hover li{height:20px}
<ul>Hover me
<li>Juice</li>
<li>Tea</li>
<li>Milk</li>
<li>Coffee</li>
</ul>

@Andrew Messier 2016-10-16 16:55:27

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

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

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

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

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

Solution as Fiddle

body {
  font-family: sans-serif;
}

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

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

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

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

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

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

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

@Andrew Messier 2016-10-22 22:11:32

Nice comparison @davidtaubmann, your pen illustrates the differences very well! I went with translateY over scale because I didn't like how scale gave that compression effect.

@DavidTaubmann 2016-10-25 17:31:09

Thanks @andrew-messier, I deleted the comment because there's a mistake, translateY isn't excecuting in codepen.io/davidtaubmann/pen/zKZvGP (because there's no child to do the move, needs a fix) ... but in this one we can perfectly see the comparison from ONLY MAX-HEIGHT and mixing it with scale (as stated on other answers): codepen.io/davidtaubmann/pen/jrdPER . This all helped me a lot with a Target methodology I'm using

@Mike S. 2016-10-12 00:12:46

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.

@Danish 2016-09-02 08:42:34

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.

@Soldeplata Saketos 2017-08-29 07:36:46

This answers to another question. Please answer to what is asked

@Danish 2017-08-30 11:24:55

@SoldeplataSaketos this answer the question i think, All i'm proposing is different approach because i have face the similar situation mentioned in question and was successfully able to solve and reuse in many projects.

@Soldeplata Saketos 2017-08-30 12:46:32

Intention is good, no doubt. But using anything that's not related with height, is totally off-topic. Imagine I suggest "instead of height, why don't you try animating the font-color? Then you make disappear the element by removing it from the DOM". You see? it would work, but it's totally unrelated

@Cruz Nunez 2016-07-01 19:12:46

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

@Helmut Emmelmann 2014-08-08 09:24:26

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

@Luca Reghellin 2016-01-25 10:47:33

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!

@sboisse 2015-12-11 16:22:23

This solutions uses javascript, but it's very trivial and it works quite well.

HTML:

<button>Toggle</button>
<div class="container collapsed">
  <div class="content">
    <div>Lorem</div>
    <div>Ipsum</div>
    <div>Dolor</div>
    <div>Sit</div>
    <div>Amet</div>
  </div>
</div>

CSS:

.container
{
  overflow: hidden;
  transition: 0.5s ease;
}

.container.collapsed
{
  height: 0 !important;
}

Javascript:

$("button").click(
    function ()
    {
        var height = $(".content").height();
        $(".container").css({height: height});
        $(".container").toggleClass("collapsed");
    });

http://jsfiddle.net/r109uz7m/ (includes a workaround to the drawback explained just below):

The only drawback is you need to refresh the size of the container if the content inside it changes. In most use cases you can work around this limitation (just like it's done in the jsfiddle).

@TylerH 2015-12-12 23:11:47

What you are missing is the question is tagged CSS and the title says "using CSS".

@sboisse 2015-12-14 15:17:20

Well, what you are missing is there is no possible solution to this problem without javascript. Most proposed answers here, particularly the ones among the top answers, do use javascript, but in a much more complicated way than the solution proposed here. I hope you downvoted all the other answers that use javascript as well.

@TylerH 2015-12-14 16:16:30

I did, but you're wrong to say that there is no possible solution to this problem without JavaScript. A cursory glance at, for example, the accepted answer (with a score of nearly 1000) will tell you that. Not to mention all the other CSS-only answers.

@sboisse 2015-12-14 16:18:55

A solution with CSS is at best a workaround (a cursory glance at the comments of that answer, for example, will tell you that) since there many drawbacks to using margin-top or min-height for the transition. Stop the bad-faith already.

@TylerH 2015-12-14 17:18:38

The only bad faith here is answering a 6-year-old question with a method that totally disregards the parameters of the question.

@TylerH 2015-12-14 17:21:01

And revenge downvoting one of my answers is pretty bad faith, too.

@sboisse 2015-12-14 19:08:41

Read the question again, and tell me where it's written "No javascript allowed or be down voted!". This solution uses CSS transitions, and correct height: auto problem with javascript. All other answers that use javascript use a unnecessarily more complicated approach to get the same result but still got upvoted, so this answer automatically becomes relevant.

@sboisse 2015-12-14 19:14:32

Down votes are for when the answer is not useful. Also, have a read about what bad faith is. Retaliating on an unjustified down vote is hardly bad faith.

@TylerH 2015-12-14 19:28:31

Questions on Stack Overflow require tags for topicality and SEO. They also require a title. In this questions title and its tags you can see CSS is specified. Further, "in CSS" is specified in the question body. How you can argue that a JS answer is okay here is, frankly, ridiculous. Also, the score of an answer does not automatically make it relevant.

@TylerH 2015-12-14 19:31:40

As for what down votes are for, you're correct. And not only is revenge down voting duplicitous and bad faith (e.g. inauthentic and deceptive. After all, what does a 100% upvoted answer on a totally separate question have to do with your answer here? Nothing.), it's also against the rules of Stack Overflow. My downvote here is to show that this answer is not useful. The question scenario is attempting something with CSS. This answer uses JavaScript, and is therefore not useful. My answer you downvoted to get petty revenge is useful to its question, so that's two helpings of bad faith.

@TylerH 2015-12-14 19:32:39

You should care less about getting back at someone for taking fake internet points away from you and more about the underlying reason behind why someone downvoted your answer.

@sboisse 2015-12-14 20:01:13

What is useful or not is determined by if it helps people solve their problem or not. I wasted a lot of time browsing all the answers of that question, only to figure out I had to spend more time to investigate and write my own solution because none of them was good enough. That's why I put it here. Had it already been there, it would definitely have been useful to me, and necessarily to other people, to see that answer higher up. But whatever. Have the last word and be "right". I can definitely see that is what you want.

@will Farrell 2012-03-17 04:40:02

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

@Adam Waite 2012-11-22 17:05:01

This didn't work on the latest Chrome Canary for me

@mindfullsilence 2014-05-12 20:00:38

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

@Josh Ribakoff 2014-03-13 23:19:41

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.

@TranslucentCloud 2018-06-26 14:01:30

The working example of this will be much appreciated.

@Oleg Vaskevich 2014-10-20 22:57:21

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

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

@Ivan Durst 2015-07-01 23:27:44

This might be helpful, but I can't tell because I don't know how to use it without a better example.

@Oleg Vaskevich 2015-07-02 00:24:14

You just pass in any DOM element (for instance, from document.getElementById('mydiv') or $('#mydiv').get()), and the function toggles between hiding/showing it. If you set up CSS transitions, the element will animate automatically.

@Oleg Vaskevich 2015-07-02 00:39:21

I added a snippet. This has the advantage of working well for dynamically-sized content.

@dudewad 2015-08-19 20:57:45

-1, since this is not "using CSS". The point of the question is for a JS-free solution. I think the "correct" answer, that's not a hack, would be this, though...

@Oleg Vaskevich 2015-08-19 22:32:46

Downvote for using JS -- seriously? Half the answers here use JS, so your downvote doesn't seem fair or necessary. Besides, this answer is still using CSS to perform the actual animation, which IMO was the point of the OP. The only thing that JS is used for is to compute the actual height, as it's impossible to do so with pure CSS (and setting min-height as in the expected answer causes issues with animation speed when the size of the list can vary greatly).

@Coderer 2017-06-23 11:28:40

Upvoted, though I think .offsetHeight isn't including margins so you'd have to make a more-complex calculation to figure out how much actual space your children will occupy.

@Sam 2019-04-29 18:41:48

This solution is elegant. It works with any height. Nothing wrong with needing javascript if it gets the job done. Thanks!

@pbatey 2013-08-17 01:46:18

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/

@VIDesignz 2015-08-01 22:35:00

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>

@dudewad 2015-08-19 20:55:19

This will work until the height of the element exceeds its width. It's the basis of how margins are calculated using percentages: they are calculated based off the width of the element. So if you have a 1000 px wide element, then an element at 1100 px will be too large for this solution to work, meaning you'd have to increase that negative top margin. Basically, it's the exact same problem as using height or max-height.

@Ivan Peshev 2015-07-21 11:19:37

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;
}

@Luca Detomi 2015-08-12 14:39:19

in jsFiddle nothing is visible, and so nothing is hoverable

@dudewad 2015-08-19 20:56:15

Also, as soon as I have an element that's 9001 pixels this solution breaks :)

@Ivan Peshev 2015-08-26 07:47:39

Yes my mistake now should be ok in [email protected] about 9001 pixels , I didn't said it is absolute solution, but it can adopted for users specific cases. We are still using it and none of 4 QAs + 4 marketeers + managers even noticed something suspicious in this solution ;).

@JonTroncoso 2015-05-19 07:25:01

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!

@BoltClock 2017-04-01 06:19:11

Usually, when somebody asks how to do something "without JavaScript", it means the solution must work on browsers that have JavaScript disabled. It doesn't mean "without writing my own scripts". So to say that your plugin can be used without using JavaScript is misleading at best - considering it's a JavaScript plugin.

@JonTroncoso 2017-04-03 21:05:22

I should clarify, When I say you don't have to use any special javascript, I mean that you don't have to write any javascript. just include the JS library and specify which attributes you want to watch in the HTML. You don't have to use fixed height css, or figure anything out. just style and go.

@robertc 2010-08-18 12:46:30

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

@Hooray Im Helping 2014-06-24 16:46:46

There are usually multiple ways to solve a problem, and not all of them are appropriate or possible. I don't know why @JedidiahHurt and Ryan Ore are trying to limit possible answers on a Q&A site. Especially considering this answer was here first. Doubly so since the accepted answer is a workaround.

@GraphicsMuncher 2014-12-24 03:20:28

The answer from @jake is neither elegant nor correct. The linked answer here is far better. It works correctly and handles all size cases - neither can be said of the accepted answer.

@Andy 2015-06-26 16:24:05

Has this changed? In the bootstrap less for collapses, I see them explicitly transitioning the height, and as far as I know it adapts to whatever height: .collapsing { position: relative; height: 0; overflow: hidden; .transition-property(~"height, visibility"); .transition-duration(.35s); .transition-timing-function(ease); }

@Andy 2015-06-26 16:30:22

Oh, you know, I bet bootstrap just computes and sets the height on the content right before it triggers the CSS animation...

@Andy 2015-06-26 16:31:53

Yup: it does this.$element[dimension](this.$element[dimension]())[0].offs‌​etHeight

@Augustin Riedinger 2018-07-06 10:24:45

Is this planned to be somehow fixed? I mean, this is pretty natural to expect to being able to transition from 0 height to auto...

@TylerH 2018-11-09 17:32:31

@Meliodas "no/you can't" is a valid and acceptable answer to questions on Stack Overflow.

@Adam 2012-03-19 00:44:51

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

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

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

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

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

<br />

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

@bfred.it 2012-06-19 16:01:12

Smarty! I like the max-height-only solution! The content I'm animating is not that flexible anyway :)

@Kiruse 2012-06-24 04:20:06

Fairly easy to beat... jQuery actually provides a .slideUp, .slideDown, and .slideToggle method that will not make the height static. Try this. It's basically just a single line, does the exact same thing and extending it to the point where it animates even when you expand the content shouldn't be too much of a challenge. Since you are using jQuery already anyways...

@Adam 2012-06-26 18:20:29

@Derija93 That's using plain old javascript timeout animations. The challenge is to use CSS3 transitions instead.

@Kiruse 2012-07-29 21:58:33

Well, yes. But why would you use "CSS3 transitions instead" if you are, in your example, already using jQuery, a library, that provides a lot of "write less, do more" code anyway? I wanted to point out that your version might look a lot better, even though more complicated, if it were NOT using jQuery so it could to run on virtually any website. I guess I worded it heavily wrong. Sorry 'bout that. ;)

@Adam 2012-07-31 18:50:24

If I were releasing this on GitHub as a plugin, then yes I totally agree. But it's just a JsFiddle proof-of-concept :)

@Mark Amery 2012-11-14 16:02:36

@Derija93 Perhaps because CSS3 transitions have (according to all sources I can find) much better performance than jQuery animations. (Actually matters hugely for my current use case, which brought me here.)

@AlbertEngelB 2013-05-07 21:43:42

@Derija93 'Cause Javascript animations run slowly compared to CSS3 transitions bro, and with jQuery animations you have to worry about the animation loop. Animate something on click, then click rapidly and watch as animations repeat themselves. This is handled with CSS3.

@Kiruse 2013-05-08 04:31:11

@Dropped.on.Caprica This is a little old now. I already said I misunderstood the concept he was trying to demonstrate. Anyways, for simple animations, .stop does the trick for the rather complex animation loop problem. Now when you animate height and color but wish to interrupt the height animation only, things become a little trickier... I do get your point.

@Jon z 2013-11-26 19:44:54

The difference between the automatic height of your element and the max-height that you choose will affect how fast the transition appears to take place, this solution isn't satisfactory.

@Maxim Balaganskiy 2018-07-17 00:44:54

Even though I ended up using vanilla js addEventListener and setting height instead of max-height I still think this as a better answer. You can also make it work with multi-level collapsibles by resetting the height to auto and removing the event listener after it's done its' job

Related Questions

Sponsored Content

100 Answered Questions

[SOLVED] How to horizontally center a <div>?

34 Answered Questions

[SOLVED] Change an HTML5 input's placeholder color with CSS

31 Answered Questions

[SOLVED] Transitions on the display: property

31 Answered Questions

[SOLVED] How to make a div 100% height of the browser window

  • 2009-10-15 21:18:43
  • mike
  • 1772807 View
  • 1939 Score
  • 31 Answer
  • Tags:   html css height

31 Answered Questions

[SOLVED] Is there a CSS parent selector?

  • 2009-06-18 19:59:36
  • jcuenod
  • 1716234 View
  • 2865 Score
  • 31 Answer
  • Tags:   css css-selectors

17 Answered Questions

[SOLVED] How do CSS triangles work?

38 Answered Questions

[SOLVED] How do I vertically center text with CSS?

40 Answered Questions

[SOLVED] How to disable text selection highlighting?

26 Answered Questions

[SOLVED] Set cellpadding and cellspacing in CSS?

19 Answered Questions

[SOLVED] Is it possible to apply CSS to half of a character?

Sponsored Content