đź›  Accordion: Animations

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

đź›  Accordion: Animations

Here’s what you’ll get by the end of this lesson:

Animated accordion.

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.

/* remove these properties */
.accordion__content {
  display: none;
}

.accordion.is-open .accordion__content {
  display: grid;
}

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;
}
Accordion spills over when height is set to zero.

And we have a problem. What we have looks broken:

  1. We can see a bit of the contents when the accordion is closed.
  2. 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.

Every accordion has contents that spill out of their  containers.

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:

  1. The content itself
  2. Padding
  3. Border
  4. Margin
The box model

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.

Border box sizing.

Calculations for height change if you set height explicitly:

  1. If height is more than padding + border, height will be the value you set.
  2. 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.

.box {
  box-sizing: border-box;
  background-color: orange;
}

This is what you will see. In this case, the total height is the height of the content (118px).

Height of the content.

Let’s add a padding of 50px and border of 50px to the box. height of the box will be 318px. (50px + 50px + 118px + 50px + 50px).

.box {
  box-sizing: border-box;
  background-color: orange;
  padding: 50px;
  border: 50px solid brown;
}
Height of the box with padding and border.

If you set height to a value larger than padding + border, height will be the actual value you set.

In this case, I set height to 500px. Content expands to 300px so height of content + padding + border equals to 500px.

.box {
  box-sizing: border-box;
  background-color: orange;
  padding: 50px;
  border: 50px solid brown;
  height: 500px;
}
Content expands to fill up extra height.

If you set height to a value smaller than padding + border, height will be padding + border.

In this case, I set height to 100px. But the computed height of the box is actually 200px.

.box {
  box-sizing: border-box;
  background-color: orange;
  padding: 50px;
  border: 50px solid brown;
  height: 100px;
}
Stated height is lesser than computed height.

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.

.box {
  box-sizing: border-box;
  background-color: orange;
  padding: 0;
  border: none;
  height: 0;
}
Content shows up even though height, padding and border are set to zero.

If you want to hide content that flows outside the box, you need to set overflow to hidden.

.box {
  box-sizing: border-box;
  background-color: orange;
  padding: 0;
  border: none;
  height: 0;
  overflow: hidden;
}
Content outside of the box gets hidden with overflow: hidden.

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.

.box {
  box-sizing: border-box;
  background-color: orange;
  padding: 50px;
  border: none;
  height: 0;
  overflow: hidden
}
Overflow: hidden does not hide the part inside padding.

In case you were wondering, overflow: hidden hides the part inside border.

.box {
  box-sizing: border-box;
  background-color: orange;
  padding: 0px;
  border: 50px solid brown;
  height: 0;
  overflow: hidden
}
Overflow: hidden hides the part inside border.

Fixing the height issue

If you want to hide contents with height, you need to set height, padding, and border to zero. You also need to set overflow to hidden.

In this case, we don’t have border, so we only need to set height and padding to zero.

.accordion__content {
  height: 0;
  padding: 0;
  overflow: hidden;
}

.accordion.is-open .accordion__content {
  height: auto;
  padding-right: 3em;
  padding-bottom: 1.5em;
  padding-left: 3em;
}
No more spillage!

But this solution is undesirable.

Why?

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>

Here are the required CSS changes:

/* Remove this */
.accordion__content {
  display: grid;
  grid-template-columns: 7.5em 1fr;
  grid-column-gap: 1.5em;
  align-items: center;
  padding-right: 3em;
  padding-bottom: 1.5em;
  padding-left: 3em;
}

.accordion.is-open .accordion__content {
  height: auto;
}
/* Replace with this */
.accordion__content {
  height: 0;
  overflow: hidden;
}

.accordion.is-open .accordion__content {
  height: auto;
}

.accordion__inner {
  display: grid;
  grid-template-columns: 7.5em 1fr;
  grid-column-gap: 1.5em;
  align-items: center;
  padding-right: 3em;
  padding-bottom: 1.5em;
  padding-left: 3em;
}

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.

.accordion__content {
  transition: height 0.3s ease-out;
}

.accordion.is-open .accordion__content {
  height: auto;
}
No animation occurs.

You can only animate height if you set a specific value:

.accordion.is-open .accordion__content {
  height: 200px;
}
Animating height from 0px to 200px

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  */
}
The difference in height between accordion__content and accordion__inner.

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.

accordionContainer.addEventListener('click', event => {
  const accordionHeader = event.target.closest('.accordion__header')
  if (accordionHeader) {
    // ...
    const accordionContent = accordionHeader.nextElementSibling
    const accordionInner = accordionContent.children[0]
  }
})

We can get the height with getBoundingClientRect.

accordionContainer.addEventListener('click', event => {
  const accordionHeader = event.target.closest('.accordion__header')
  if (accordionHeader) {
    // ...
    const accordionContent = accordionHeader.nextElementSibling
    const accordionInner = accordionContent.children[0]
    const height = accordionInner.getBoundingClientRect().height
  }
})

Setting the correct height

You can change height by setting the height property with JavaScript. Remember to add px because JavaScript height from getBoundingClientRect is a pixel value.

accordionContainer.addEventListener('click', event => {
  const accordionHeader = event.target.closest('.accordion__header')
  if (accordionHeader) {
    // ...

    const accordionContent = accordionHeader.nextElementSibling
    const accordionInner = accordionContent.children[0]
    const height = accordionInner.getBoundingClientRect().height

    accordionContent.style.height = height + 'px'
  }
})
Opening the accordion with animation.

Closing the accordion

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
accordionContainer.addEventListener('click', event => {
  const accordionHeader = event.target.closest('.accordion__header')
  if (accordionHeader) {
	  // ...

    if (accordion.classList.contains('is-open')) {
      accordionContent.style.height = height + 'px'
    } else {
      accordionContent.style.height = 0
    }
  }
})

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.

let height

if (accordion.classList.contains('is-open')) {
  height = 0
} else {
  height = accordionInner.getBoundingClientRect().height
}

accordion.classList.toggle('is-open')
accordionContent.style.height = height + 'px'
Closing the accordion with animation.

In short, follow this general rule of thumb: Change the DOM only at the end (after you calculated everything you need).

A little aside

Remember to remove is-open from the HTML. We should start with the accordion closed.

<div class="accordion is-open"> ... </div>
<div class="accordion"> ... </div>
<div class="accordion"> ... </div>
<div class="accordion"> ... </div>

Note: If you want to start with accordions opened, you need to calculate height when the page loads.

Wrapping up

Three things:

  1. When you animate height, you create Jank. Sometimes there’s no choice. Either you animate or you don’t.
  2. Animate 1 property if you can. Don’t animate 2 (or more) properties that cause Jank.
  3. You might need to change the HTML to create smooth animations. If this is required, go for it.