Leveraging JavaScript to implement CSS transitions that use display: none
Tags: animation, css, javascript
Published: 2022-02-13 22:50:37

---
title: Leveraging JavaScript to implement CSS transitions that use display: none
published: true
description: Use JavaScript to toggle "display: none" when a CSS transition animation completes
tags: animation, css, js
//cover_image: https://direct_url_to_image.jpg
---

CSS can't natively animate transitions that use `display: none`. You can hack around this limitation by using a mix of `visibility: hidden` and `height: 0` to make it "close enough." While these solutions are probably fine in most cases, it isn't quite the same as using `display: none`.

This post will show you a method for combining `display: none` with CSS transitions that trigger the `display: none` CSS property using JavaScript.

## What we're building

We'll build a box that transitions from `opacity: 1` to `opacity: 0` when a button is clicked, then when the transition is complete, we'll toggle from the initial display property to `display: none` using JavaScript. Here's what the final result will look like:

{% codesandbox r85ri %}

## The code

Below is the code to implement the animated transition seen above:

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link href="/src/app.css" />
    <script src="/src/app.js" defer></script>
  </head>

  <body>
    <div id="box" class="box"></div>
    <div>
      <button id="toggler">Toggle visibility</button>
    </div>
  </body>
</html>
```

```css
/** app.css */

.box {
  opacity: 1;
  height: 100px;
  width: 100px;
  background: lightblue;
  margin-bottom: 20px;
  transition: opacity 1s;
}
.box--hidden {
  opacity: 0;
}
```

```js
/** app.js */

const toggler = document.getElementById("toggler");
const toggleBox = document.getElementById("box");
const isHidden = () => toggleBox.classList.contains("box--hidden");

toggleBox.addEventListener("transitionend", function () {
  if (isHidden()) {
    toggleBox.style.display = "none";
  }
});

toggler.addEventListener("click", function () {
  if (isHidden()) {
    toggleBox.style.removeProperty("display");
    setTimeout(() => toggleBox.classList.remove("box--hidden"), 0);
  } else {
    toggleBox.classList.add("box--hidden");
  }
});

```

## How it works

Our code toggles the CSS class `.box--hidden` when the toggle button is clicked, which sets the box's opacity to 0. The `.box` class has a `transition` property that will animate the transition between states.

```css
/** app.css */

.box {
  opacity: 1;
  height: 100px;
  width: 100px;
  background: lightblue;
  margin-bottom: 20px;
  transition: opacity 1s;
}
.box--hidden {
  opacity: 0;
}
```

Neither the `.box` class nor the `.box--hidden` class have a `display` property: this property will be set within JavaScript.

Our script includes a callback that executes when the `transitionend` event is fired on the box. If the box includes the `.box--hidden` class, it will set the box's CSS to `display: none`, hiding the box once the transition animation is complete.

```js
toggleBox.addEventListener("transitionend", function () {
  if (isHidden()) {
    toggleBox.style.display = "none";
  }
});
```

On the click handler that fires at the end of the transition,  it will check to see if the box is currently hidden. If it is hidden, it will removed the `display: none` style applied by the previously mentioned callback, then it will set a zero-second timeout before removing the `box--hidden` class. Without the zero-second timeout, the browser will render the box immediately with no transition. While it's not important to understand all of the reasons behind this, just know that it is not a race condition, but instead has to do with the browser being single-threaded, meaning that the browser must first have a chance to render the updates.

Conversely, if the box does not have the `.box--hidden` class, the callback will apply it.

```js
toggler.addEventListener("click", function () {
  if (isHidden()) {
    toggleBox.style.removeProperty("display");
    setTimeout(() => toggleBox.classList.remove("box--hidden"), 0);
  } else {
    toggleBox.classList.add("box--hidden");
  }
});
```

## Recommendation: use a library instead

If you're reading this and thinking that the code looks fragile: I agree with you. The HTML, CSS and JS are tightly-coupled, and if you needed to update a class name you'd need to change it in all three files. 

The animation can also break in interesting ways. For example, if you have a zero-second transition, the `transitionend` event will never fire, which means `display: none` will never be applied.

Instead of hand-wiring these animations, consider using a library that makes animations practical. jQuery's `.fadeToggle()` method creates a comparable transition to the one we implemented in this post using a single line of code. Alpine.js and Vue let you apply different CSS classes for each stage of a transition animation. In many front-end frameworks, you can completely remove elements from the DOM after an animation ends rather than relying on `display: none` to hide it.

While reducing the number of dependencies within a project is a worthy endeavor, sometimes their conveniences make them well worth including.