DEV Community

Cover image for How to animate an element in display none in two steps
David Pollet
David Pollet

Posted on

How to animate an element in display none in two steps

Animate a hidden element is very simple now. We just need 2 CSS declaration and a bit of JavaScript to toggle the state open / close.

The solution

In this article:

  • CSS :not pseudo-selector
  • Single Source of truth
  • animationend event
  • Accessibility

Problem we try to solve

You have a hidden element with a hiddenattribute. To provide a better UX, you want to animate this opening and closing state. But in CSS, display:none is like an interrupter; it can be "on" or "off" but animate between both state with a CSS transition is impossible.

Step 1: animate during opening

The HTML

<a href="#modal" class="modal-button">Open Modal</a>
<div class="modal" id="modal" hidden>Modal content</div>
Enter fullscreen mode Exit fullscreen mode

As you notice, I'm opting for a link instead of a button. Why ? Because my modal will still be accessible without JavaScript, thanks to the target CSS pseudo-class. I will focus on this point after.

JS to show the modal

modalButton.addEventListener("click", function (e) {
  e.preventDefault();
  modal.hidden = false;
});
Enter fullscreen mode Exit fullscreen mode

CSS

.modal:not([hidden]) {
  animation-name: popIn;
}
Enter fullscreen mode Exit fullscreen mode

That's it. ¯_(ツ)_/¯.
If you prefer relying on .hidden class (like in Tailwind), you can switch :not([hidden]) with :not(.hidden). If you want both, the not pseudo-class accept multiple arguments separated by a comma : not([hidden], .hidden). Anyway, our Modal appears with a shiny animation now :

Step 2 : animate during closing

The closing state is a little more tricky. If you set the hidden attribute to "true", you won't be able to hide it smoothly. You need to add a temporary class like is-closing to play the closing animation and then, hide the element.

JS

modal.addEventListener("click", function (e) {
  // Omitted…
  if (hasClickedOutside || hasClickedCloseButton) {
    modal.classList.add("is-closing");
    // Omitted…
Enter fullscreen mode Exit fullscreen mode

CSS

.modal.is-closing {
  animation-name: popOut;
}
Enter fullscreen mode Exit fullscreen mode


Now our modal is closing smoothly, but it is not back to hidden state. You have to wait to the end of the animation to remove the .is-closing class and back to hidden="true". With setTimeout ? You could, but you have a better option.

Animationend event

With a timeout, we have to declare a value at least equal to the animation duration, which can change.
If you can, you have to have a single source of truth : here, the animation duration declared in the CSS.
The animationend will wait to the end of the animation, then execute the function inside the listener.

modal.addEventListener("click", function (e) {
  const hasClickedOutside = !e.target.closest(".modal-main");
  const hasClickedCloseButton = e.target.closest(".modal-close");

  if (hasClickedOutside || hasClickedCloseButton) {
    modal.classList.add("is-closing");

    modal.addEventListener(
      "animationend",
      function () {
        modal.hidden = true;
        modal.classList.remove("is-closing");
      },
      { once: true }
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

Once the event is completely done, you have to destroy it with the once: true option, as you don't need it anymore.
And voilà, you have the knowledge to animate any element hidden in the DOM.

Bonus : A little accessibility enhancement

Button vs Link

As I said above, I choose a <a> instead of a <button> because of that :

.modal:target {
  display: grid !important;
  animation-name: popIn;
  .modal-close {
    display: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

Without JS, the modal can still be open via its hash, and you can style the opened state with the :target pseudo class.
To close it, the user needs to back in your history. This is why I hide the .modal-close. It's not pertinent to show it if it can't do anything.

Don't play animation if user don't want animation.

For personal taste, medical reason or to solve a performance issue on their device, your users may not want any animation, and you have to respect their preferences. It would be a good idea to embed the following the rule as part of your CSS reset, if it's not already done.

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for reading. 👋

Discussion (0)