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

animationen…

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>

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;
});

CSS

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

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…

CSS

.modal.is-closing {
  animation-name: popOut;
}



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 }
    );
  }
});

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;
  }
}

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;
  }
}

Thanks for reading. 👋


Print Share Comment Cite Upload Translate
APA
David Pollet | Sciencx (2024-03-28T11:05:00+00:00) » How to animate an element in display none in two steps. Retrieved from https://www.scien.cx/2022/06/29/how-to-animate-an-element-in-display-none-in-two-steps/.
MLA
" » How to animate an element in display none in two steps." David Pollet | Sciencx - Wednesday June 29, 2022, https://www.scien.cx/2022/06/29/how-to-animate-an-element-in-display-none-in-two-steps/
HARVARD
David Pollet | Sciencx Wednesday June 29, 2022 » How to animate an element in display none in two steps., viewed 2024-03-28T11:05:00+00:00,<https://www.scien.cx/2022/06/29/how-to-animate-an-element-in-display-none-in-two-steps/>
VANCOUVER
David Pollet | Sciencx - » How to animate an element in display none in two steps. [Internet]. [Accessed 2024-03-28T11:05:00+00:00]. Available from: https://www.scien.cx/2022/06/29/how-to-animate-an-element-in-display-none-in-two-steps/
CHICAGO
" » How to animate an element in display none in two steps." David Pollet | Sciencx - Accessed 2024-03-28T11:05:00+00:00. https://www.scien.cx/2022/06/29/how-to-animate-an-element-in-display-none-in-two-steps/
IEEE
" » How to animate an element in display none in two steps." David Pollet | Sciencx [Online]. Available: https://www.scien.cx/2022/06/29/how-to-animate-an-element-in-display-none-in-two-steps/. [Accessed: 2024-03-28T11:05:00+00:00]
rf:citation
» How to animate an element in display none in two steps | David Pollet | Sciencx | https://www.scien.cx/2022/06/29/how-to-animate-an-element-in-display-none-in-two-steps/ | 2024-03-28T11:05:00+00:00
https://github.com/addpipe/simple-recorderjs-demo