Create a Pure-CSS Expanding DIV

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.

Open / Close
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.

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:

Open / Close
  • One
  • Two
  • Three
  • We can hide the checkbox and replace it with a label element, which will function as our toggle button.

  • One
  • Two
  • Three
  • 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.

    A collection of construction signs under an overpass.
    Cows on a hill by the beach.
    A lobster in Edinburgh.

    Start

    This website is made with Svelte, which allows you to create really robust websites really easily. For this project, I’ve combined Svelte with Markdown. You know when you put asterisks around words to make them italic in whatsapp? That’s Markdown.

    That means that I can write something like this:

    <script>
      let product = 5 * 5
    </script>
    
    > The **product** of five times five is {product}.

    And it will appear on the page like this:

    The product of five times five is 25.

    In fact, you can look at the code for this page on GitHub. You’ll be surprised by how simple it looks.

    © Sam Littlefair, 2022