Reputation: 92661
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>
Upvotes: 2980
Views: 1909946
Reputation: 17554
height: calc-size(auto)
As of 2024
, there is a new CSS
function called calc-size()
, which calculates the intrinsic size values of the element, like auto
, fit-content
, and max-content
.
Using calc-size(auto)
will measure the actual height
of the element, the same as if it were hardcoded.
HTML
<button id="toggle">Toggle</button>
<div class="accordion">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ullamcorper luctus diam. Phasellus cursus ultrices rhoncus. Nam non nunc nec lorem aliquam ultrices vel quis lacus. Cras id lorem sit amet libero egestas consequat non vehicula metus. In quis gravida dui, quis egestas nunc. Fusce vitae dictum sem. Cras vel nisi fermentum, congue justo id, luctus mauris. Maecenas scelerisque libero at dictum lacinia. Maecenas accumsan facilisis lectus at euismod.
Etiam fringilla, purus non mollis pharetra, tellus est sodales urna, sed aliquam lacus dolor sit amet est. Integer facilisis, augue sit amet accumsan vulputate, justo sem mattis arcu, id ultricies risus mauris at lectus. Donec vitae blandit mauris. Nam placerat augue a ligula imperdiet, vel lacinia risus volutpat. Vivamus id purus eget libero cursus rutrum. Integer id facilisis mauris. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam tempor finibus erat et malesuada. Nam sagittis vestibulum consequat. Mauris scelerisque orci vitae elit tincidunt, quis feugiat augue venenatis. Mauris pellentesque ligula at metus tincidunt vehicula.
</div>
CSS
.accordion {
height: 0;
transition: height 0.5s;
overflow: hidden;
}
.accordion.show {
height: auto; /* just as backup if browser doesn't support calc-size */
height: calc-size(auto);
}
JS
const toggleButton = document.getElementById('toggle');
const accordion = document.querySelector('.accordion');
toggleButton.addEventListener('click', () => {
accordion.classList.toggle('show');
})
As for now, this property is experimental, and the only way to test it is to download Chrome Canary and enable "Experimental Web Platform features"
MDN
Can Use Support
The Official repository for the CSS Working Group
Upvotes: 3
Reputation: 75757
You can't currently animate on height when one of the heights involved is auto
, you have to set two explicit heights.
Update: As per the comment below there is now support for animating elements with intrinsic sizes in Chrome: https://developer.chrome.com/blog/new-in-chrome-129#animate
If you need a workaround for other/older browsers see the highly upvoted answers above.
Upvotes: 291
Reputation: 273807
Now we have a new CSS property that enable transition to height: auto
without any effort or hacky code.
interpolate-size: allow-keywords
Here is a simplified example taken from my blog: https://css-tip.com/animate-height-auto/ (chrome only for now)
& {
interpolate-size: allow-keywords; /* define at root level to enable the auto transition for all the elements */
}
p {
transition: 1s;
}
p:not(:hover) {
height: 5lh;
}
/* extra styling */
p {
max-width: 400px;
outline: 2px solid red;
font-size: 25px;
border: 10px solid #0000;
overflow: hidden;
cursor: pointer;
}
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean venenatis molestie purus. Sed venenatis pulvinar eleifend. Nam facilisis nec lacus et sagittis. Sed eleifend eu risus non aliquet. Morbi volutpat mi et libero pulvinar placerat. Phasellus laoreet sed augue id venenatis. Ut non turpis nisi. Mauris nec varius libero. Donec sed fringilla felis. Morbi placerat nisi id augue viverra fermentum.</p>
Applying the same technique to your example
& {
interpolate-size: allow-keywords;
}
#child0 {
height: 0;
overflow: hidden;
background-color: #dedede;
transition: height 1s ease;
}
#parent0:hover #child0 {
height: auto;
}
#child40 {
height: 40px;
overflow: hidden;
background-color: #dedede;
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>
Upvotes: 12
Reputation: 109
Big news! 🎉
It is now possible (since June 2024) to actually animate the height, instead of using the "max-height"-workaround!
Now you can just use the "calc-height" css-function like this:
.panel {
height: 0;
transition: height 0.25s ease-in;
&.expanded {
height: calc-size(auto);
}
}
But be careful, this function is still experimental and it is unlikely that this is the final syntax.
For more detailed information visit:
Upvotes: 2
Reputation: 317
Flexible Height CSS Only Solution
I've stumbled upon a quirky solution using flex behavior. It works in at least Chrome and Firefox.
First, the height transition only works between 0 and 100%, two numeric values. Since "auto" is not a numeric value, fractional increments don't exist between 0 and "auto". 100% is a flexible value, so no specific height is required.
Second, both the outer container and the inner container of the hidden content must be set to display: flex with flex-direction: column.
Third, the outer container must have a height property. Setting it to 0 maintains a smooth transition only when everything is contained in the outer container because the flex behavior takes precedence over the height. Edit: Paintoshi suggested using height: fit-content, so that any content below the container is also pushed down.
.outer-container { height: 0; display: flex; flex-direction: column; }
.inner-container { display: flex; flex-direction: column; }
.hidden-content {
height: 0;
opacity: 0;
transition: height 1s 0.5s ease-in-out, opacity 0.5s ease-in-out;
/* transition out: first fade out opacity, then shrink height after a delay equal to the opacity duration */
}
.trigger:hover + .inner-container > .hidden-content {
height: 100%;
opacity: 1;
transition: height 1s ease-in-out, opacity 0.5s 1s ease-in-out;
/* transition in: first expand height, then fade in opacity after a delay equal to the height duration */
}
<div class="outer-container">
<a href="#" class="trigger">Hover to Reveal Inner Container's Hidden Content</a>
<div class="inner-container">
<div class="hidden-content">This is hidden content. When triggered by hover, its height transitions from 0 to 100%, which pushes other content in the same container down gradually.</div>
<div>Within the same container, this other content is pushed down gradually as the hidden content's height transitions from 0 to 100%.</div>
</div>
</div>
Press the Run Code Snippet button to see the transition in action. It's CSS only, with no specific height.
Upvotes: 12
Reputation: 834
I do not recommend my original response, but I will leave it for posterity. Ever since flexbox became widely supported, I have relied on it for solutions to issues like the one in this question.
I recommend this answer, which uses a flex container and a transition between height: 0
and height: 100%
to avoid any sort of "magic values".
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.
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>
Upvotes: 29
Reputation: 3737
I liked the solution of jake and dotnetCarpenter. But jake's solution lacked the control on transition and dotnetCarpenter's solution does not remove the space. So I kinda fused those two solution which works fine for me. This is what I got:
---------------------- css class based solution ------------------------
HTML:
<div>
<div id="title">
Show items
</div>
<ul class="theul hideul">
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
</div>
CSS:
ul{list-style: none;}
.hide {
max-height: 0;
transform: scale(1, 0);
transform-origin: top;
transition: transform 0.3s ease-in, max-height 0.3s ease-in;
> li {
transform: scale(1, 0);
transition: transform 0.3s ease-in;
}
}
.show {
max-height: 999px;
transform: scale(1, 1);
transform-origin: top;
transition: transform 0.3s ease-in, max-height 0.3s ease-in;
> li {
transform: scale(1, 1);
transition: transform 0.3s ease-in;
}
}
Add event listener on #title
to toggle between hide
and show
class.
---------------------- only css solution ------------------------
if you dont' want any of javascript, then this is only css solution(same solution):
HTML:
<div>
<div id="title">
Show items
</div>
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
</div>
CSS:
ul {
list-style: none;
max-height: 0;
transform: scale(1, 0);
transform-origin: top;
transition: transform 0.3s ease-out, max-height 0.3s ease-in-out;
/* transition-delay: 0.1s; */
> li {
transform: scale(1, 0);
transition: transform 0.3s ease-in;
}
}
#title2:hover + ul {
max-height: 999px;
transform: scale(1, 1);
transform-origin: top;
transition: transform 0.3s ease-in, max-height 0.3s ease-in-out;
/* transition-delay: 0.1s; */
> li {
transform: scale(1, 1);
transition: transform 0.3s ease-in;
}
}
Sidenote: If you dont care about transition, then details
is one of the best way to write dropdown ui.
HTML
<details>
<summary>
<p>show items</p>
</summary>
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
</details>
if you want to add a custom dropdown icon, then remove the default icon first:
summary::marker {
content: none;
}
Upvotes: 3
Reputation: 16204
Browsers have moved on a fair bit since this question was first raised. You can now transition grid track sizes and it is very well supported meaning that this problem can be solved using CSS Grid Layout
Here I am transitioning the second grid item from 0fr
(0 Fractional Units) to 1fr
(1 Fractional Units).
#parent0 {
display: grid;
grid-template-rows: min-content 0fr;
transition: grid-template-rows 500ms;
}
#child0 {
background-color: #dedede;
overflow: hidden;
}
#parent0:hover{
grid-template-rows: min-content 1fr;
}
<div id="parent0">
<h1>Hover me</h1>
<div id="child0">Some content
<br>Some content
<br>Some content
<br>Some content
<br>Some content
</div>
</div>
Upvotes: 81
Reputation: 1678
I wanted to share the method I ended up using (and haven't seen yet)
Let me start by saying it is not possible to translate to auto
without javascript:
It CAN work with CSS transitions but requires some JavaScript to change auto
the `scrollheight
The way it works:
overflow: hidden
.scrollHeight
.0px
in the next animation frametoggle()
toggles the collapsed
class just to track the stateThis way the CSS transition will work because it is made from 0px
to scrollHeight
instead of auto
.
function collapse(el, collapse) {
el.style.height = el.scrollHeight+'px';
if(!collapse)
requestAnimationFrame(() => el.style.height = '0px');
}
function toggle(btn) {
const ul = btn.nextElementSibling,
collapsed = ul.classList.contains('collapsed');
collapse(ul, collapsed);
ul.classList.toggle('collapsed');
}
ul {
transition: height 0.16s ease-out;
overflow:hidden;
height:auto;
}
<ul>
<li>Item 1
<li>Item 2
<li>Item 3
</ul>
<button onclick="toggle(this)">Toggle</button>
<ul>
<li>Item 4
<li>Item 5
<li>Item 6
</ul>
<ul>
<li>Item 7
<li>Item 8
<li>Item 9
</ul>
Upvotes: -1
Reputation: 6850
According to MDN Web Docs, auto
values have been intentionally excluded from the CSS transitions spec, so instead of height: auto
, use height: 100%
, top
, or the flex
property in grid and flex layouts.
.grid-container {
display: grid;
position: absolute;
}
.content {
background: aqua;
height: 0;
overflow: hidden;
transition: 1s;
}
span:hover + .grid-container .content {
height: 100%;
}
<span>Hover over me!</span>
<div class="grid-container">
<div class="content">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>
</div>
<p>Rest of the page content...</p>
.grid-container {
display: grid;
position: absolute;
overflow: hidden;
pointer-events: none; /* to enable interaction with elements below the container */
}
.content {
background: aqua;
pointer-events: auto;
position: relative;
top: -100%;
transition: 1s;
}
span:hover + .grid-container .content {
top: 0;
}
<span>Hover over me!</span>
<div class="grid-container">
<div class="content">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>
</div>
<p>Rest of the page content...</p>
Upvotes: 41
Reputation: 452
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
Update: CSS has evolved to include much more elegant techniques since I wrote this answer. I believe the best today is by using css grid and grid-template-row as described in this answer: https://stackoverflow.com/a/69802208/3594432
Upvotes: 8
Reputation: 8249
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.
What you want: http://codepen.io/csuwldcat/pen/kwsdF
Slap the following JS file into your page, and after that add/remove a single boolean attribute--reveal=""
--from the nodes you want to expand and contract.
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>
Drop this JS file in your page:
/*** 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);
Upvotes: 3
Reputation: 4783
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>
Upvotes: 22
Reputation: 881
Set the style=""
attributes for tracked elements whenever there's a change in the DOM. plugin called mutant-transition You can use CSS for your transitions and not use hacks. You don't have to write any JavaScript. Just include the JavaScript library and specify which attributes you want to watch in the HTML. You don't have to use fixed height CSS. 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>
This uses MutationObserver to follow changes in the DOM. You don't have to set anything up or use JavaScript to manually animate. Changes are tracked automatically. However, because it uses MutationObserver, this will only transition in IE11+. < IE11 will see snap-changes (no transition).
Fiddles
height: auto
to height: 100%
height: auto
when adding childrenUpvotes: 6
Reputation: 2094
A lot of answers, some better than other, most using JS. I believe I figured this out in two use-cases that are easy to understand.
.demo01 {
overflow: hidden;
position: absolute;
pointer-events: none;
}
.demo01__content {
background: lightgray;
padding: 1rem;
pointer-events: all;
transform: translateY(-100%);
transition: transform 1s, visibility 1s;
visibility: hidden;
}
:checked ~ .demo01 .demo01__content {
transform: translateY(0);
visibility: visible;
}
<input type="checkbox" /> ⬅︎ Toggle
<div>Something before 🙃</div>
<div class="demo01">
<div class="demo01__content">
This content should…<br />
toggle! 👻
</div>
</div>
<div>Something after 🙂</div>
.demo02 {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition: grid-template-rows 1s;
}
.demo02__content {
align-self: end;
min-height: 0;
background: lightgray;
transition: visibility 1s;
visibility: hidden;
}
.demo02__padding {
padding: 1rem;
}
:checked ~ .demo02 {
grid-template-rows: 1fr;
}
:checked ~ .demo02 .demo02__content {
visibility: visible;
}
<input type="checkbox" /> ⬅︎ Toggle
<div>Something before 🙃</div>
<div class="demo02">
<div class="demo02__content">
<div class="demo02__padding">
This content should…<br />
toggle! 👻
</div>
</div>
</div>
<div>Something after 🙂</div>
I wrote a blog post about these techniques.
Upvotes: 19
Reputation: 675
I want to add an example about how to expanding/collapsing keeping the doc flow, this example is for React apps using tailwindcss
export function Collapse({ collapsed = true, children }) {
return (
<div className="grid">
<div className="flex flex-col">
<div className={`transition-all duration-500 overflow-hidden ${collapsed ? 'basis-0' : 'flex-1'}`}>
{children}
</div>
</div>
</div>
);
}
To go deep read: https://stackoverflow.com/a/69871346/9226510
Upvotes: 1
Reputation: 26954
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.The upshot though is that we get performant transitioning using CSS only, and a single transition function to smoothly achieve the transition; the holy grail!
Of course, there's a downside! I can't work out how to control the width at which content gets cut off (overflow:hidden
); because of the padding-bottom hack, the width and height are intimately related. There may be a way though, so will come back to it.
https://jsfiddle.net/EoghanM/n1rp3zb4/28/
body {
padding: 1em;
}
.trigger {
font-weight: bold;
}
/* .expander is there for float clearing purposes only */
.expander::after {
content: '';
display: table;
clear: both;
}
.outer {
float: left; /* purpose: shrink to fit content */
border: 1px solid green;
overflow: hidden;
}
.inner {
transition: padding-bottom 0.3s ease-in-out; /* or whatever crazy transition function you can come up with! */
padding-bottom: 0%; /* percentage padding is defined in terms of width. The width at this level is equal to the height of the content */
height: 0;
/* unfortunately, change of writing mode has other bad effects like orientation of cursor */
writing-mode: vertical-rl;
cursor: default; /* don't want the vertical-text (sideways I-beam) */
transform: rotate(-90deg) translateX(-100%); /* undo writing mode */
transform-origin: 0 0;
margin: 0; /* left/right margins here will add to height */
}
.inner > div { white-space: nowrap; }
.expander:hover .inner, /* to keep open when expanded */
.trigger:hover+.expander .inner {
padding-bottom: 100%;
}
<div class="trigger">HoverMe</div>
<div class="expander">
<div class="outer">
<div class="inner">
<div>First Item</div>
<div>Content</div>
<div>Content</div>
<div>Content</div>
<div>Long Content can't be wider than outer height unfortunately</div>
<div>Last Item</div>
</div>
</div>
</div>
<div>
after content</div>
</div>
Upvotes: 8
Reputation: 3663
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>
Upvotes: 58
Reputation: 50948
This is a CSS-only solution with the following properties:
transform: scaleY(0)
), so it does the right thing if there's content after the collapsible element.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.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>
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:
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.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.
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:
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.
Upvotes: 174
Reputation:
The approach I use is to have everything based on font-size (using em as unit) or at least everything that affects the vertical size of the box we want to animate open. Then if anything isn't in em units (for example a 1px border) I set that to 0 when the box is closed, by targetting all children with *.
What the animation animates is the font-size in % from 0 to 100. Of what? Of a known font-size of the parent.
The rules for it to work are that everything in the animated box must:
border-top:0;border-bottom:0;
)In a way you can still use pixels as reference unit, by simply setting the wrapper font-size to 100px e.g. #page{font-size:100px;}
so if you want 10px anywhere inside you can use 0.1em
.
This is not the prettiest thing anyone can write, but hey, these browsers don't give us any beautiful solution to this problem. As soon as the height of the box is unpredictable we have no choice but to get somewhat dirty, and this is the least dirty thing I came up with.
Hover version:
https://jsfiddle.net/xpsfkb07/1/
#page {
font-size: calc( (35vw + 65vh) / 30); /* just some responsive design as a bonus */
}
#slidebox {
background-color: #e8e8e8;
visibility: hidden;
font-size: 0; /* animated from 0 to 100% */
opacity: 0; /* optional */
transition: 0.5s;
}
a#ahover:hover ~ #slidebox {
visibility: visible;
font-size: 100%; /* animated from 0 to 100% */
opacity: 1; /* optional */
}
a#ahover:not(:hover) ~ #slidebox * {
border-top: 0;
border-bottom: 0;
/* Put here anything vertical that uses px as unit, in this case the borders */
}
a#button {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
width: 20em;
height: 3em;
padding: 0.5em;
border: 2px solid #0080ff;
border-radius: 0.4em;
background-color: #8fbfef;
color: #404040;
box-sizing: border-box;
}
#someform {
margin-top: 1em;
margin-bottom: 1em;
padding: 1em 4em 1em 4em;
background-color: #d8ffd8;
border: 1px solid #888888;
}
#someform input {
display: inline-block;
box-sizing: border-box;
font-size: 125%;
padding: 0.5em;
width: 50%; /* anything horizontal can still be in px or % */
border-radius: 0.4em;
border: 1px solid red;
}
<div id=page>
<a id=ahover href="#">Hover me</a><br>
Here is the box that slides:
<div id=slidebox>
I am the content of the slide box (line1).<br>
I am the content of the slide box (line2).<br>
<a id=button href="#">I am some button in the slide box</a><br>
I am the content of the slide box (line3).<br>
I am the content of the slide box (line4).
<div id=someform>
Some box with a form or anything...<br>
<input type=text value="Text Box">
</div>
I am the content of the slide box (line5).<br>
I am the content of the slide box (line6).
</div>
And this is after the box.
</div>
Class change version:
https://jsfiddle.net/8xzsrfh6/
const switch_ele = document.getElementById('aclass');
switch_ele.addEventListener('click', function(){
const box_ele = document.getElementById('slidebox');
box_ele.className = box_ele.className == 'show' ? 'hide' : 'show';
}, true);
#page {
font-size: calc( (35vw + 65vh) / 30); /* just some responsive design as a bonus */
}
#slidebox {
background-color: #e8e8e8;
visibility: hidden;
font-size: 0; /* animated from 0 to 100% */
opacity: 0; /* optional */
transition: .5s;
}
#slidebox.show {
visibility: visible;
font-size: 100%; /* animated from 0 to 100% */
opacity: 1; /* optional */
}
#slidebox.hide * {
border-top: 0;
border-bottom: 0;
/* Put here anything vertical that uses px as unit, in this case the borders */
}
a#button {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
width: 20em;
height: 3em;
padding: 0.5em;
border: 2px solid #0080ff;
border-radius: 0.4em;
background-color: #8fbfef;
color: #404040;
box-sizing: border-box;
}
#someform {
margin-top: 1em;
margin-bottom: 1em;
padding: 1em 4em 1em 4em;
background-color: #d8ffd8;
border: 1px solid #888888;
}
#someform input {
display: inline-block;
box-sizing: border-box;
font-size: 125%;
padding: 0.5em;
width: 50%; /* anything horizontal can still be in px or % */
border-radius: 0.4em;
border: 1px solid red;
}
<div id=page>
<a id=aclass href="#">Switch class w/ js</a><br>
Here is the box that slides:
<div id=slidebox class=hide>
I am the content of the slide box (line1).<br>
I am the content of the slide box (line2).<br>
<a id=button href="#">I am some button in the slide box</a><br>
I am the content of the slide box (line3).<br>
I am the content of the slide box (line4).
<div id=someform>
Some box with a form or anything...<br>
<input type=text value="Text Box">
</div>
I am the content of the slide box (line5).<br>
I am the content of the slide box (line6).
</div>
And this is after the box.
</div>
Upvotes: 0
Reputation: 11331
You should use scaleY instead.
ul {
background-color: #eee;
transform: scaleY(0);
transform-origin: top;
transition: transform 0.26s ease;
}
p:hover ~ ul {
transform: scaleY(1);
}
<p>Hover This</p>
<ul>
<li>Coffee</li>
<li>Tea</li>
<li>Milk</li>
</ul>
I've made a vendor prefixed version of the above code on jsfiddle, and changed your jsfiddle to use scaleY instead of height.
Edit
Some people do not like how scaleY
transforms the content. If that is a problem then I suggest using clip
instead.
ul {
clip: rect(auto, auto, 0, auto);
position: absolute;
margin: -1rem 0;
padding: .5rem;
color: white;
background-color: rgba(0, 0, 0, 0.8);
transition-property: clip;
transition-duration: 0.5s;
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
h3:hover ~ ul,
h3:active ~ ul,
ul:hover {
clip: rect(auto, auto, 10rem, auto);
}
<h3>Hover here</h3>
<ul>
<li>This list</li>
<li>is clipped.</li>
<li>A clip transition</li>
<li>will show it</li>
</ul>
<p>
Some text...
</p>
Upvotes: 499
Reputation: 1908
Alternate CSS-only solution with line-height
, padding
, opacity
and margin
:
body {
background-color: linen;
}
main {
background-color: white;
}
[id^="toggle_"] ~ .content {
line-height: 0;
opacity: 0;
padding: 0 .5rem;
transition: .2s ease-out;
}
[id^="toggle_"] ~ .content > p {
margin: 0;
transition: .2s ease-out;
}
[id^="toggle_"]:checked ~ .content {
opacity: 1;
padding: .5rem;
line-height: 1.5;
}
[id^="toggle_"]:checked ~ .content p {
margin-bottom: .75rem;
}
[id^="toggle_"] + label {
display: flex;
justify-content: space-between;
padding: 0.5em 1em;
background: lightsteelblue;
border-bottom: 1px solid gray;
cursor: pointer;
}
[id^="toggle_"] + label:before {
content: "Show";
}
[id^="toggle_"]:checked + label:before {
content: "Hide";
}
[id^="toggle_"] + label:after {
content: "\25BC";
}
[id^="toggle_"]:checked + label:after {
content: "\25B2";
}
<main>
<div>
<input type="checkbox" id="toggle_1" hidden>
<label for="toggle_1" hidden></label>
<div class="content">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dolor neque, commodo quis leo ut, auctor tincidunt mauris. Nunc fringilla tincidunt metus, non gravida lorem condimentum non. Duis ornare purus nisl, at porta arcu eleifend eget. Integer lorem ante, porta vulputate dui ut, blandit tempor tellus. Proin facilisis bibendum diam, sit amet rutrum est feugiat ut. Mauris rhoncus convallis arcu in condimentum. Donec volutpat dui eu mollis vulputate. Nunc commodo lobortis nunc at ultrices. Suspendisse in lobortis diam. Suspendisse eget vestibulum ex.
</p>
</div>
</div>
<div>
<input type="checkbox" id="toggle_2" hidden>
<label for="toggle_2" hidden></label>
<div class="content">
<p>
Maecenas laoreet nunc sit amet nulla ultrices auctor. Vivamus sed nisi vitae nibh condimentum pulvinar eu vel lorem. Sed pretium viverra eros ut facilisis. In ut fringilla magna. Sed a tempor libero. Donec sapien libero, lacinia sed aliquet ut, imperdiet finibus tellus. Nunc tellus lectus, rhoncus in posuere quis, tempus sit amet enim. Morbi et erat ac velit fringilla dignissim. Donec commodo, est id accumsan cursus, diam dui hendrerit nisi, vel hendrerit purus dolor ut risus. Phasellus mattis egestas ipsum sed ullamcorper. In diam ligula, rhoncus vel enim et, imperdiet porta justo. Curabitur vulputate hendrerit nisl, et ultricies diam. Maecenas ac leo a diam cursus ornare nec eu quam.
</p>
<p>Sed non vulputate purus, sed consectetur odio. Sed non nibh fringilla, imperdiet odio nec, efficitur ex. Suspendisse ut dignissim enim. Maecenas felis augue, tempor sit amet sem fringilla, accumsan fringilla nibh. Quisque posuere lacus tortor, quis malesuada magna elementum a. Nullam id purus in ante molestie tincidunt. Morbi luctus orci eu egestas dignissim. Sed tincidunt, libero quis scelerisque bibendum, ligula nisi gravida libero, id lacinia nulla leo in elit.
</p>
<p>Aenean aliquam risus id consectetur sagittis. Aliquam aliquam nisl eu augue accumsan, vel maximus lorem viverra. Aliquam ipsum dolor, tempor et justo ac, fermentum mattis dui. Etiam at posuere ligula. Vestibulum tortor metus, viverra vitae mi non, laoreet iaculis purus. Praesent vel semper nibh. Curabitur a congue lacus. In et pellentesque lorem. Morbi posuere felis non diam vulputate, non vulputate ex vehicula. Vivamus ultricies, massa id sagittis consequat, sem mauris tincidunt nunc, eu vehicula augue quam ut mauris.
</p>
</div>
</div>
<div>
<input type="checkbox" id="toggle_3" hidden>
<label for="toggle_3" hidden></label>
<div class="content">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dolor neque, commodo quis leo ut, auctor tincidunt mauris. Nunc fringilla tincidunt metus, non gravida lorem condimentum non. Duis ornare purus nisl, at porta arcu eleifend eget. Integer lorem ante, porta vulputate dui ut, blandit tempor tellus. Proin facilisis bibendum diam, sit amet rutrum est feugiat ut. Mauris rhoncus convallis arcu in condimentum. Donec volutpat dui eu mollis vulputate. Nunc commodo lobortis nunc at ultrices. Suspendisse in lobortis diam. Suspendisse eget vestibulum ex.
</p>
<p>Sed non vulputate purus, sed consectetur odio. Sed non nibh fringilla, imperdiet odio nec, efficitur ex. Suspendisse ut dignissim enim. Maecenas felis augue, tempor sit amet sem fringilla, accumsan fringilla nibh. Quisque posuere lacus tortor, quis malesuada magna elementum a. Nullam id purus in ante molestie tincidunt. Morbi luctus orci eu egestas dignissim. Sed tincidunt, libero quis scelerisque bibendum, ligula nisi gravida libero, id lacinia nulla leo in elit.
</p>
</div>
</div>
</main>
Upvotes: 5
Reputation: 57
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.
Upvotes: 4
Reputation: 6543
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>
Upvotes: 9
Reputation: 69
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,
Here's the result
[...document.querySelectorAll('.ab')].forEach(wrapper => {
wrapper.addEventListener('click', function () {
this.classList.toggle('active');
});
});
* {
margin: 0;
box-sizing: border-box;
}
.c {
overflow: hidden;
}
.items {
width: 100%;
visibility: hidden;
max-height: 0;
margin-bottom: -2000px;
-webkit-transition: margin 0.6s cubic-bezier(1, 0, 1, 1), max-height 0s 0.6s linear, visibility 0s 0.6s linear;
transition: margin 0.6s cubic-bezier(1, 0, 1, 1), max-height 0s 0.6s linear, visibility 0s 0.6s linear;
}
.items > * {
padding: 1rem;
background-color: #ddd;
-webkit-transition: background-color 0.6s ease;
transition: background-color 0.6s ease;
}
.items > *:hover {
background-color: #eee;
}
.ab {
padding: 1rem;
cursor: pointer;
background: #eee;
}
.ab.active + .c .items {
max-height: 2000px;
margin-bottom: 0;
visibility: visible;
-webkit-transition: max-height 0.6s cubic-bezier(1, 0, 1, 1);
transition: max-height 0.6s cubic-bezier(1, 0, 1, 1);
}
.dropdown {
margin-right: 1rem;
}
.wrapper {
display: -webkit-box;
display: flex;
}
<div class="wrapper">
<div class="dropdown">
<div class="ab">just text</div>
<div class="ab">just text</div>
<div class="ab">dropdown</div>
<div class="c">
<div class="items">
<p>items</p>
<p>items</p>
<p>items asl;dk l;kasl;d sa;lk</p>
<p>items sal;kd</p>
<p>items</p>
</div>
</div>
<div class="ab">just text</div>
<div class="ab">just text</div>
</div>
<div class="dropdown">
<div class="ab">dropdown</div>
<div class="c">
<div class="items">
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
<p>items</p>
</div>
</div>
<div class="ab">text</div>
</div>
<div class="dropdown">
<div class="ab">placeholder</div>
<div class="ab">dropdown</div>
<div class="c">
<div class="items">
<p>items</p>
<p>items</p>
</div>
</div>
<div class="ab">placeholder</div>
<div class="ab">placeholder</div>
<div class="ab">placeholder</div>
</div>
</div>
<h1>text to be pushed</h1>
Upvotes: 1
Reputation: 522
One sentence solution: Use padding transition. It's enough for most of cases such as accordion, and even better because it's fast due to that the padding value is often not big.
If you want the animation process to be better, just raise the padding value.
.parent{ border-top: #999 1px solid;}
h1{ margin: .5rem; font-size: 1.3rem}
.children {
height: 0;
overflow: hidden;
background-color: #dedede;
transition: padding .2s ease-in-out, opacity .2s ease-in-out;
padding: 0 .5rem;
opacity: 0;
}
.children::before, .children::after{ content: "";display: block;}
.children::before{ margin-top: -2rem;}
.children::after{ margin-bottom: -2rem;}
.parent:hover .children {
height: auto;
opacity: 1;
padding: 2.5rem .5rem;/* 0.5 + abs(-2), make sure it's less than expected min-height */
}
<div class="parent">
<h1>Hover me</h1>
<div class="children">Some content
<br>Some content
<br>Some content
<br>Some content
<br>Some content
<br>Some content
<br>
</div>
</div>
<div class="parent">
<h1>Hover me(long content)</h1>
<div class="children">Some content
<br>Some content<br>Some content
<br>Some content<br>Some content
<br>Some content<br>Some content
<br>Some content<br>Some content
<br>Some content<br>Some content
<br>
</div>
</div>
<div class="parent">
<h1>Hover me(short content)</h1>
<div class="children">Some content
<br>Some content
<br>Some content
<br>
</div>
</div>
Upvotes: 11
Reputation: 1541
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.
Upvotes: 1
Reputation: 2247
You could do this by creating a reverse (collapse) animation with clip-path.
#child0 {
display: none;
}
#parent0:hover #child0 {
display: block;
animation: height-animation;
animation-duration: 200ms;
animation-timing-function: linear;
animation-fill-mode: backwards;
animation-iteration-count: 1;
animation-delay: 200ms;
}
@keyframes height-animation {
0% {
clip-path: polygon(0% 0%, 100% 0.00%, 100% 0%, 0% 0%);
}
100% {
clip-path: polygon(0% 0%, 100% 0.00%, 100% 100%, 0% 100%);
}
}
<div id="parent0">
<h1>Hover me (height: 0)</h1>
<div id="child0">Some content
<br>Some content
<br>Some content
<br>Some content
<br>Some content
<br>Some content
<br>
</div>
</div>
Upvotes: 9
Reputation: 37912
Use max-height
in the transition 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>
Upvotes: 3700
Reputation: 1525
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.
Upvotes: 16