At Prismic, the sidebar in our navigation has some accordian elements. When you click on them, they expand smoothly and a group of children items appear, using only CSS.
An expanding div
is a classic effect, which basically looks like this:
Click me
Seeing this effect in the Prismic docs made me want to try to implement it myself, but I could never find a solution I liked. Most solutions rely on max-height
, which — though I won’t get into it here — I’m not a fan of.
Then, yesterday, I saw a tweet from @Steve8708 showing an example where Apple uses this effect, but without explaining how they achieve it. Steve challenged readers to figure it out themselves.
The basic mechanism relies on a common trick where the checkbox modifies the style of its siblings. This works because the checkbox state can be accessed with the :checked
pseudo selector. But changing the height of an element, specifically, is much harder.
This is difficult because of the way that web browsers understand an object’s inherent size and a manipulated size: they’re completely different. It’s like the difference between measuring coffee in milliliters and mugs. I might have a mug of coffee, but I can’t double my coffee by getting another mug, because the other mug might be a different size. Instead, I must measure the coffee in milliliters and double the milliliters.
Similarly, web browsers mostly don’t allow relative sizing of elements. You can’t directly say, “Make this element twice as big.” So, instead, you have to measure the size of the element and use math and JavaScript to resize it. That basic operation is probably one of the most common uses for jQuery — the infamous JavaScript library that dominated the web through the aughts.
Here’s the operative (Svelte) code for that box above:
<script>
let open = false
</script>
<div class="first" class:open on:click={() => open = !open}>
Click me
</div>
<style>
.first {
height: 50px; // Initial height is hard-coded
border: 2px solid black;
transition: height 1s;
}
.open {
height: 100px; // Final height is hard-coded
}
</style>
As you can see, the initial and final height are hard-coded, which is bad practice.
I’ve spent a lot of time trying to crack this problem. There must be a way to do this without JavaScript!
There is!
It turns out, there is one relative CSS unit that can achieve this pretty nicely: line-height
.
Here’s the relevant code for that example (this is just HTML with CSS):
<input type="checkbox" />
<strong>Open / Close</strong>
<div class="collapse">Lorem ipsum...</div>
<style>
.collapse {
transition: line-height 1s ease-out, opacity 0.6s linear;
line-height: 1.5;
margin: 0;
overflow: hidden;
opacity: 1;
}
:checked ~ .collapse {
line-height: 0;
opacity: 0;
}
</style>
I’ve combined line-height
with opacity
to prevent a mess when the lines overlap. However, if you don’t have any word wrap, you can prevent this with overflow-hidden
:
We can hide the checkbox and replace it with a label
element, which will function as our toggle button.
Here’s the code for that one:
<input class="hidden" id="toggle" type="checkbox" />
<label for="toggle">Open / Close</label>
<div class="collapse">
<li>One</li>
<li>Two</li>
<li>Three</li>
</div>
<style>
.collapse {
transition: line-height 1s ease-out;
line-height: 1.5;
margin: 0;
overflow: hidden;
opacity: 1;
}
:checked ~ .collapse {
line-height: 0;
opacity: 0;
}
.hidden {
display: none;
}
label {
font-weight: 700;
cursor: pointer;
}
</style>
That’s a pure-CSS animated div
.
See the skeleton code and styled code on CodePen.
→