In-N-Out Animations: Dialogs (Part 1/3)
In-N-Out Animations: Dialogs (Part 1/3) ź“ė Ø
Iād like to kick off a small series here focused on animating elements in and out of view.
First, weāre going to focus on an element that goes from display: none; to display: block;. This is of particular interest because, well, it used to be quite difficult to do. But more than that: itās often highly desirable. Movement can help a user understand whatās going when a new element appears or disappears.
Article Series
Letās start with the modern wonder that is the <dialog> element.
Just want the final code snippet?! Jump here.
By default, a <dialog> is display: none; and when you open it, it becomes display: block; naturally. We donāt have to write these styles. If weāre animating something else between these values, thatās fine, youāll just need to do it yourself:
.thing {
display: none;
&.open {
display: block;
}
}
If youāre like me, that triggers something in your brain that says well now you canāt animate it, bud. And indeed, if you tried like this:
.thing {
display: none;
opacity: 0;
transition: 1s opacity,
&.open {
display: block;
opacity: 1;
}
}
That transition would indeed not work. If you toggled that open class, the element would immediately appear (and disappear).
Letās swap over to <dialog> styles, as thatās what weāll be animating the rest of the time.
dialog {
opacity: 0;
transition: 1s opacity,
&:open {
opacity: 1;
}
}
Note
Note the difference of not hand-writing the display change and using the :open pseudo-class.
Hereās a video of where we are at:
Introducing allow-discrete
The first trick weāre going to need to employ is using a special keyword as the transition-behavior. Here it is:
dialog {
opacity: 0;
transition:
1s opacity,
1s display allow-discrete;
&:open {
/* as it first renders,
it's already this. */
opacity: 1;
}
}
Iām applying the transition-behavior as part of the shorthand value there.
Iām sure that name, allow-discrete, has some fancy reason for being named that, but I donāt know what that is, and I find the name pretty rough (it doesnāt help me understand it). But we need it, so cāest la vie.
The problem we were facing (with the lack of animation) is that the display property changes at an inopportune time during the timeline. We want to change it such that when weāre changing to display: block; we want that to happen right away then let the opacity transition (in our case). Vice versa, when weāre changing to display: none; we want it to wait to change until the transition is over. Hereās a diagram of that:

With that in place, well, hereās a movie of what happens:
Thatās still⦠not great.
Letās be clear here about our goals and how weāre doing so far:
ā Dialog instantly appears
ā Backdrop instantly appears
ā
Dialog fades out
ā Backdrop instantly hides
So 1 out of 4. Weāve got more work to do. Letās get that dialog fading in.
Using @starting-style
The way I understand the dialog not fading in, so far, is that when the dialog renders on the page for the very first time, the :open styles immediately apply to it, which are display: block and opacity: 1 already, so there is no need/time to animate anything.
So we need to be very specific with styles for that pre-animation state. One way to do that is applying a @keyframe animation and letting it run to completion, which will happen when the element first renders. But I donāt love that way because it feels more like trickery than an explicit choice to me, and more importantly, the animation isnāt āinteruptableā (see explanation in code here) (phaux).
The way to be specific about ābefore openā styles is a CSS things called @starting-style. It looks like this:
dialog {
opacity: 0;
transition:
1s opacity,
1s display allow-discrete;
&:open {
opacity: 1;
}
@starting-style {
&:open {
opacity: 0;
}
}
}
This is basically saying, hey, when you first render on the page, itās actually got these styles, and if there happens to be any animation/transition applied, they will happen from these values. Which is exactly what we need.
That gets us here:
A bit better. Weāve checked another one off the list:
ā
Dialog fades in
ā Backdrop instantly appears
ā
Dialog fades out
ā Backdrop instantly hides
We just havenāt dealt with that backdrop yet, and you can really feel it.
But before we go there, we need to take some stock into what weāre doing here.
@starting-style is LAST
Notice Iāve put the styles for @starting-style at the end of our block of code styling the dialog element.
dialog {
opacity: 0;
transition:
1s opacity,
1s display allow-discrete;
&:open {
opacity: 1;
}
@starting-style {
&:open {
opacity: 0;
}
}
}
Thatās very on purpose.
Those styles need to override the :open styles, but @starting-style doesnāt add any specificity, so they must come later in order to successfully override.
Closed Styles
A bit of a strange thing has emerged here in that the styles that are directly applied to dialog end up being the styles when the dialog isnāt open. Meaning when it comes to animation, the āon the way outā styling, or the styles that the animation moves to as the dialog is being closed.
We can group those styles more clearly, like this:
dialog {
transition:
1s opacity,
1s display allow-discrete;
&:not(:open) {
opacity: 0;
}
&:open {
opacity: 1;
}
@starting-style {
&:open {
opacity: 0;
}
}
}
The *Three State* System
Now we have the styles for this dialog isolated into three distinct chunks.
- The āOn The Way Outā Styles
- The āOpenā Styles
- The āOn The Way Inā Styles
Thatās how they are in the source order. But if weāre going to re-number them as the user would experience them, Iād actually reverse the order.

The process of opening-then-closing the dialog is like this illustration below, hence the ābackwardsā ordering:

Note
If you remember anything from this post, I think remembering the three-state system is the most important. When dealing with in-and-out styling, you have the opportunity to style all three states, and itās best to put them in reverse source order.
Three States is Cool!
Styling all three states here is all but a requirement. But instead of having it feel like a repetitive burden, think of it as an opportunity to do some unique design work. Is the āon the way inā style bigger (like scale: 1.1;)?, then maybe the the āon the way outā style could be smaller (like scale: 0.9;). It could even have different anchor points (chriscoyier)!
The Backdrop
In order to accomplish our four goals, we still need to deal with the styling behind the dialog, known as the backdrop, and selected in CSS with ::backdrop.
There is a lot to get right here:
- The
::backdropneeds itās own transitions - It needs another special keyword
- It needs all three states of styles added
Keeping it simple and only dealing with an opacity fade in/out, it looks like this:
dialog {
transition:
1s opacity,
1s display allow-discrete,
1s overlay allow-discrete;
&::backdrop {
transition: opacity 1s;
}
&:not(:open) {
opacity: 0;
&::backdrop {
opacity: 0;
}
}
&:open {
opacity: 1;
&::backdrop {
opacity: 1;
}
}
@starting-style {
&:open {
opacity: 0;
&::backdrop {
opacity: 0;
}
}
}
}
Notice weāre now applying a transition to the overlay property which*⦠isnāt actually a property?*
The overlay keyword is another thing that I canāt say I fully get. It has something to do with enabling ātop layerā animations, of which dialog and ::backdrop are a part. Iām not a fan, as it feels like something quite random and obscure that doesnāt quite jive with how other things work in CSS. If you donāt include it here, the ::backdrop will not animate out smoothly. But note that we need to apply it to the dialog, not the ::backdrop itself even though it only affects the ::backdrop. š¤·āāļø. I did sit in a CSSWG meeting where they were discussing removing it, so weāll see.
With all this in place: weāre in business:
ā
Dialog fades in
ā
Backdrop fades in
ā
Dialog fades out
ā
Backdrop fades out
Demo
Allow me to add a few more basic styles so it feels a bit more real, and tada:
Reduced Motion
Note that on this demo, weāve got an @media query to accommodate users who prefer reduced motion:
@media (prefers-reduced-motion: reduce) {
dialog {
transition:
1s opacity,
1s display allow-discrete,
1s overlay allow-discrete;
&, &:open {
translate: 0;
}
}
}
Iāve leaving in the opacity animation, just removing the actual movement, which I think is the proper spirit of this user setting.
Just The Snippet
Looking for a starting point to copy and paste? Here ya go:
dialog {
transition:
1s opacity,
1s display allow-discrete,
1s overlay allow-discrete;
&::backdrop {
transition:
opacity 1s,
display 1s allow-discrete,
overlay 1s allow-discrete;
}
&:not(:open) {
opacity: 0;
&::backdrop {
opacity: 0;
}
}
&:open {
opacity: 1;
&::backdrop {
opacity: 1;
}
}
@starting-style {
&:open {
opacity: 0;
&::backdrop {
opacity: 0;
}
}
}
}
@media (prefers-reduced-motion: reduce) {
dialog {
/* If you add animation that isn't just opacity, remember to remove it here. */
/* transition:
1s opacity,
1s display allow-discrete,
1s overlay allow-discrete; */
}
}
Conclusion
I feel like you get it. Three states. Style them all. Get all the little details right, including the source order, so it actually works. The reason I wrote this post is that itās actually kinda easy to get those details wrong and have something not animate that you think should be. And because of the history with these features, it not working can feel like a hard limitation, like maybe it just canāt work. Well, it can. Hopefully these demos and explanations can help.
And one more thing.
Wouldnāt it be kinda cool to be able to abstract this kind of thing away with a CSS @mixin or something? I think that is still being shaken out, so while thatās true, Iāll just make up some fake code I think would be cool. I wonāt include how the @mixin would be written, just how Iād want to use it once it has, and how this would expand into our final snippet is an exercise for authors.
@mixin --in-and-out-animation(
--transition-properties: <array>,
--in-style: <style-block>,
--open-style: <style-block>,
--out-style: <style-block>,
--timing: <duration>,
--backdrop: <boolean>
) {
... magic ...
}
dialog {
@apply --in-and-out-animation(
--transition-properties: opacity, translate, scale,
--in-style: {
opacity: 0;
translate: 100px 0;
scale: 1.1;
}
--open-style {
opacity: 1;
translate: 0;
scale: 1;
}
--out-style: {
opacity: 0;
translate: -100px 0;
scale: 0.9;
}
--timing: 350ms;
--backdrop: true;
);
}
Article Series