DEV Community

Tyler Smith
Tyler Smith

Posted on • Updated on

Leveraging JavaScript to implement CSS transitions that use display: none

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:

The code

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

<!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>
Enter fullscreen mode Exit fullscreen mode
/** app.css */

.box {
  opacity: 1;
  height: 100px;
  width: 100px;
  background: lightblue;
  margin-bottom: 20px;
  transition: opacity 1s;
}
.box--hidden {
  opacity: 0;
}
Enter fullscreen mode Exit fullscreen mode
/** 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");
  }
});

Enter fullscreen mode Exit fullscreen mode

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.

/** app.css */

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

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.

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

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.

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

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.

Top comments (4)

Collapse
 
rossangus profile image
Ross Angus

Thanks, Tyler: this was exactly what I needed. Shame this isn't possible in pure CSS.

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm glad this helped! I sure wish that this worked with pure CSS too.

Collapse
 
dottenpixel profile image
DouG Molidor

To help the case of 0-second transitions not firing transitiionend, set your transition-duration to something like 0.001 to ensure the event fires, but is visual imperceivable.

Collapse
 
tohodo profile image
Tommy

You can simplify JS and get rid of setTimeout by using an animation (does not require JS to fade in, requires animationend callback to fade out).