Here’s what you’ll get by the end of this lesson:
Let’s start with the most important lesson of all.
Don’t use display none
You cannot animate an element if you hide it with display: none. The display property cannot be animated. Since display cannot be animated, we need a different way to hide the content.
First, we’ll remove display: none from the CSS code.
There are many ways to hide content. For example you can:
Push it off the screen (like Off-canvas menu)
Make it invisible (like Modal)
In this case, we will animate the height property. Animating height causes jank, but there are no other methods for animating an accordion.
First, we need to set height to zero.
.accordion__content {
height: 0;
}
And we have a problem. What we have looks broken:
We can see a bit of the contents when the accordion is closed.
The contents spill out of the accordion for “Beef”.
Contents actually spill out of every accordion. You only see contents spill out of “Beef” because I set position: relative to .accordion. If you remove the position: relative from .accordion, here’s what you’ll see.
To understand why this happens, we need to take a detour into CSS.
Calculating Height (and Width)
In CSS, every element is drawn with the CSS Box Model. The Box Model contains four things:
The content itself
Padding
Border
Margin
Normally, height values are calculated based on the box-sizing property:
If you set box-sizing to content-box (it’s a default value), height is the height of the content only
If you set box-sizing to border-box, height is the height of the content + any paddings and borders you have
To make things simpler, I’m going to explain the problem with height in terms of box-sizing: border-box from this point onwards.
Calculations for height change if you set height explicitly:
If height is more than padding + border, height will be the value you set.
If height is less than padding + border, browsers will set height to padding + border.
This makes more sense if we explain with an example. Let’s say you have the following HTML.
<div class="box">
<p>The quick brown fox jumps over the lazy dog</p>
</div>
You set the box-sizing property to border-box. You also add a background-color to the box.
Here, content does not have any height at all. It is 0px tall.
You can still see the content because browsers set overflow to visible by default. This means: when content is smaller than it’s allocated amount, allow content to flow outside of the box and be visible.
You can see the content even if you set height, padding and border to 0.
But here’s a strange thing with padding. If the box has padding, any content within the padding area is considered to be part of the box. overflow: hidden does not hide this part.
If you animate the accordion with this solution, you need to animate both height and padding properties. Both height and padding cause jank. Animating one of them is bad enough. Animating both of them at the same time will make the animation lag.
We want to reduce jank as much as we can. The ideal solution here is to animate height only.
If we want to animate only height, we need to adjust the HTML. Here, we can wrap the contents inside another <div>. We can put padding inside this <div>, so .accordion__content does not contain any more padding.
<div class="accordion__content">
<div class="accordion__inner">
<!-- The contents -->
</div>
</div>
Quick Tip: You often need to rewrite the HTML to create animations. I wanted to show you this process so you don’t beat yourself up when you encounter the same problem!
Animating height
height can be animated, but you cannot animate height property to auto.
If you want to animate height, you need to use JavaScript to find (and set) the correct height for each accordion.
Getting the correct height
We can get the height of an element with getBoundingClientRect, but we need to know which element to get the height from.
In this case, we want to get height from .accordion__inner. The height remains accurate even if we set height on .accordion__content to zero.
.accordion__content {
height: 0;
}
.accordion.is-open .accordion__content {
/* Don't set any height property here yet */
}
To get the height from .accordion__inner, you need to traverse the DOM to find .accordion__inner.
Here, we know .accordion__content is the next element from .accordion__header. We also know .accordion__inner is the first element inside .accordion__content.
You can change height by setting the height property with JavaScript. Remember to add px because JavaScript height from getBoundingClientRect is a pixel value.
To close the accordion, you need to set height back to zero. You need to do this because styles set by JavaScript are inline-styles. And inline styles have a higher specificity compared to a class in CSS.
Here, you use an if statement to check whether the accordion is currently open.
If the accordion is open, you need to close the accordion, so you set height to 0
If the accordion is closed, you need to open the accordion, so you set height to the value you got from getBoundingClientRect
Important note: The code above works only if classList.toggle comes after it.
// This works
if (accordion.classList.contains('is-open')) {
accordionContent.style.height = 0
} else {
accordionContent.style.height = height + 'px'
}
accordion.classList.toggle('is-open')
// This does not work
accordion.classList.toggle('is-open')
if (accordion.classList.contains('is-open')) {
accordionContent.style.height = 0
} else {
accordionContent.style.height = height + 'px'
}
Why? Because classList.toggle changes the HTML when you use it.
// Let's say accordion has `is-open` now
accordion.classList.toggle('is-open')
// aaccordion does not have `is-open` anymore. It was removed by classList.toggle.
To make the code clearer, you can calculate height upfront first.