TIL a better way to handle in/out transitions
Tags: css, animations, javascript, webdev
Published: 2020-04-20 06:26:34

Using CSS grid & the hidden attribute to simplify in/out transitions.

If I need to replace an element with another element, it's often a good user experience if there's a transition. A card gets replaced with another card, maybe a carousel, whatever. 

In other words, I want to animate something that disappears and, _at the same time and place_, animate another thing that appears.

I always found coding this to be a bit clunky because there are two annoying problems:
* Two elements need to be positioned in the same space. 
* I will need to keep the disappearing element around during the animation; I can’t remove it at the moment it makes the most sense.

This morning I came across a [tweet](https://twitter.com/DavidKPiano/status/1207308369687699456) by [David K. Piano](https://twitter.com/DavidKPiano) that got me all excited. He offers solutions to both these problems, way better solutions than I’ve ever used before!

{% twitter 1207308369687699456 %}

## How to position 2 elements in the same space?

CSS and the DOM aren’t really good at positioning 2 elements in the same place[*](#except-svg). There are not many ways to accomplish this. 

Previously I’ve used  `position: absolute` on those 2 elements. This works well, but now both elements are taken out of the layout flow, so don’t take any space anymore. To combat this you could read the height and width of these elements and set these on their parent. But what if the dimensions change? Add a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)? The code gets quite complex for something so basic.

That’s why I was so excited when I read David’s solution: using CSS Grid to create overlapping elements 😲. 

```css
.parent {
    display: grid;
    grid-template: 1/1;
}

.transitioningChildren {
    grid-area: 1/1;
}
```

### So what’s going on here?

We are telling the parent to make a grid with one row and one column. The children are all positioned in the area of the grid, which occupies the 1st row and the 1st column. 

This will stack all the `.transitioningChildren` on top of each other. 💥Boom💥.

And what’s more: the grid will automatically expand to the width of its widest child and to the height of its highest child (weird sentence, but ok…). Freaking genius!!!

I absolutely love how something designed for a completely different reason (grid layouts), fits so well in this use case (positioning elements on top of each other for animation).

<a name="except-svg">*</a>) Except for SVG, elements inside an SVG stack on top of each other by default.

## How to animate something that’s disappearing?

It isn’t possible to animate something that’s not there. 

In order to work around this, I have seen solutions where both the appearing and disappearing elements are kept around during the animation. The disappearing element is removed after the animation is complete. React-spring’s [`useTransition`](https://www.react-spring.io/docs/hooks/use-transition) and [`ReactTransitionGroup`](https://reactcommunity.org/react-transition-group/) work this way. 

But this technique isn’t without its drawbacks: screen readers see both elements, I could tab to a link or button in an element that’s disappearing, and so on.

I could throw more code at it to solve these issues, but it’s a hassle.

And what if I just want a simple CSS transition and don’t want the added complexity of these tools?

The magic that these tools add is keeping the disappearing element around for long enough to finish the animation. But what if I could just leave the element in the DOM? If only it would not interfere with screen readers, keyboard navigation, layout, blah blah blah. This would make my life a lot easier.

Turns out we can use the [`hidden`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/hidden) attribute for all these requirements. And what’s even more impressive is how we can use the `hidden` attribute as a __selector__ and transition from and to the hidden state.

The hidden attribute sets `display: none` in the browser's stylesheet. So we do need to explicitly declare another display property on the element to override it.

```css
.transitioningChild {
    display: block;
    transition: all .3s .3s cubic-bezier(.5, 0, .5, 1);
    transition-property: opacity, transform;
}

.transitioningChild[hidden] {
    transition-delay: 0;
    opacity: 0;
    transform: scale(0.8);
}
```

Whuuut! Awesome!

I would definitely use this in situations where I don’t mind keeping hidden elements around.  

> It’s okay to keep elements in the DOM. It’s not like you have to pay DOM rent for them to stay there.

Here’s a demo showing both these principles:
{% codesandbox funny-wind-p59ei %}

### Browser support

This holds up surprisingly well in modern browsers, even in IE11! It uses an older spec for CSS Grid, but with a few tweaks, the result is the same.

```css
.example {
    display: -ms-grid;
    display: grid;
    -ms-grid-rows: 1;
    -ms-grid-columns: 1;
        grid-template: 1/1;
}

.child {
    -ms-grid-row: 1;
    -ms-grid-column: 1;
    grid-area: 1/1;
}
```
The hidden attribute is supported as well in all modern browsers and IE11.

### Conclusion
I’ve used grid-areas, and -templates before, I knew about the hidden attribute, but I never put together how they could work together to help with in/out transitions.

If you find this useful, follow [David](https://twitter.com/DavidKPiano) (or [me](https://twitter.com/joostkiens)😅) on Twitter for more tips like these.