How to animate an element in display none in two steps
Tags: css
Published: 2022-06-29 14:53:18

---
title: How to animate an element in display none in two steps
published: true
description: Learn to open and close smoothly element that need a display none
tags: CSS
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7gv1r54ht0exthfcvjnq.png
# Use a ratio of 100:42 for best results.
---
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
{% codepen https://codepen.io/davidpollet/pen/gOeOMbb %}

## 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 `hidden`attribute. 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 :

{% youtube https://youtu.be/Yt2DtIjZNHU %}

## 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;
}
```
{% youtube https://youtu.be/pEvC9CE2wb8 %}
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. 👋