@starting-style
and transition-behavior
— two properties which can be completely welcome additions to your on a regular basis work with CSS animations.Animating from and to show: none;
was one thing we may solely obtain with JavaScript to vary courses or create different hacks. The explanation why we couldn’t do that in CSS is defined within the new CSS Transitions Stage 2 specification:
“In Stage 1 of this specification, transitions can solely begin throughout a mode change occasion for components which have an outlined before-change type established by the earlier type change occasion. Which means a transition couldn’t be began on a component that was not being rendered for the earlier type change occasion.”
In easy phrases, which means that we couldn’t begin a transition on a component that’s hidden or that has simply been created.
What Does transition-behavior: allow-discrete
Do?
allow-discrete
is a little bit of a wierd identify for a CSS property worth, proper? We’re occurring about transitioning show: none
, so why isn’t this named transition-behavior: allow-display
as a substitute? The reason being that this does a bit greater than dealing with the CSS show
property, as there are different “discrete” properties in CSS. A easy rule of thumb is that discrete properties don’t transition however normally flip straight away between two states. Different examples of discrete properties are visibility
and mix-blend-mode
. I’ll embody an instance of those on the finish of this text.
To summarise, setting the transition-behavior
property to allow-discrete
permits us to inform the browser it might swap the values of a discrete property (e.g., show
, visibility
, and mix-blend-mode
) on the 50% mark as a substitute of the 0% mark of a transition.
What Does @starting-style
Do?
The @starting-style
rule defines the kinds of a component proper earlier than it’s rendered to the web page. That is extremely wanted together with transition-behavior
and because of this:
When an merchandise is added to the DOM or is initially set to show: none
, it wants some kind of “beginning type” from which it must transition. To take the instance additional, popovers and dialog components are added to a high layer which is a layer that’s outdoors of your doc move, you’ll be able to form of have a look at it as a sibling of the <html>
component in your web page’s construction. Now, when opening this dialog or popover, they get created inside that high layer, so that they don’t have any kinds to begin transitioning from, which is why we set @starting-style
. Don’t fear if all of this sounds a bit complicated. The demos would possibly make it extra clearly. The necessary factor to know is that we may give the browser one thing to begin the animation with because it in any other case has nothing to animate from.
A Notice On Browser Assist
In the meanwhile of writing, the transition-behavior
is out there in Chrome, Edge, Safari, and Firefox. It’s the identical for @starting-style
, however Firefox at present doesn’t help animating from show: none
. However do not forget that every part on this article will be completely used as a progressive enhancement.
Now that we’ve got the idea of all this behind us, let’s get sensible. I’ll be overlaying three use circumstances on this article:
- Animating from and to
show: none
within the DOM. - Animating dialogs and popovers coming into and exiting the highest layer.
- Extra “discrete properties” we will deal with.
Animating From And To show: none
In The DOM
For the primary instance, let’s check out @starting-style
alone. I created this demo purely to elucidate the magic. Think about you need two buttons on a web page so as to add or take away record gadgets within an unordered record.
This could possibly be your beginning HTML:
<button sort="button" class="btn-add">
Add merchandise
</button>
<button sort="button" class="btn-remove">
Take away merchandise
</button>
<ul function="record"></ul>
Subsequent, we add actions that add or take away these record gadgets. This may be any methodology of your selecting, however for demo functions, I shortly wrote a little bit of JavaScript for it:
doc.addEventListener("DOMContentLoaded", () => {
const addButton = doc.querySelector(".btn-add");
const removeButton = doc.querySelector(".btn-remove");
const record = doc.querySelector('ul[role="list"]');
addButton.addEventListener("click on", () => {
const newItem = doc.createElement("li");
record.appendChild(newItem);
});
removeButton.addEventListener("click on", () => {
if (record.lastElementChild) {
record.lastElementChild.classList.add("eradicating");
setTimeout(() => {
record.removeChild(record.lastElementChild);
}, 200);
}
});
});
When clicking the addButton
, an empty record merchandise will get created within the unordered record. When clicking the removeButton
, the final merchandise will get a brand new .eradicating
class and at last will get taken out of the DOM after 200ms.
With this in place, we will write some CSS for our gadgets to animate the eradicating half:
ul {
li {
transition: opacity 0.2s, remodel 0.2s;
&.eradicating {
opacity: 0;
remodel: translate(0, 50%);
}
}
}
That is nice! Our .eradicating
animation is already wanting excellent, however what we had been in search of right here was a solution to animate the entry of things coming within our DOM. For this, we might want to outline these beginning kinds, in addition to the ultimate state of our record gadgets.
First, let’s replace the CSS to have the ultimate state within that record merchandise:
ul {
li {
opacity: 1;
remodel: translate(0, 0);
transition: opacity 0.2s, remodel 0.2s;
&.eradicating {
opacity: 0;
remodel: translate(0, 50%);
}
}
}
Not a lot has modified, however now it’s as much as us to let the browser know what the beginning kinds needs to be. We may set this the identical means we did the .eradicating
kinds like so:
ul {
li {
opacity: 1;
remodel: translate(0, 0);
transition: opacity 0.2s, remodel 0.2s;
@starting-style {
opacity: 0;
remodel: translate(0, 50%);
}
&.eradicating {
opacity: 0;
remodel: translate(0, 50%);
}
}
}
Now we’ve let the browser know that the @starting-style
ought to embody zero opacity and be barely nudged to the underside utilizing a remodel
. The ultimate result’s one thing like this:
However we don’t must cease there! We may use totally different animations for coming into and exiting. We may, for instance, replace our beginning type to the next:
@starting-style {
opacity: 0;
remodel: translate(0, -50%);
}
Doing this, the gadgets will enter from the highest and exit to the underside. See the total instance on this CodePen:
See the Pen [@starting-style demo – up-in, down-out [forked]](https://codepen.io/smashingmag/pen/XJroPgg) by utilitybend.
When To Use transition-behavior: allow-discrete
Within the earlier instance, we added and eliminated gadgets from our DOM. Within the subsequent demo, we’ll present and conceal gadgets utilizing the CSS show
property. The essential setup is just about the identical, besides we’ll add eight record gadgets to our DOM with the .hidden
class hooked up to it:
<button sort="button" class="btn-add">
Present merchandise
</button>
<button sort="button" class="btn-remove">
Cover merchandise
</button>
<ul function="record">
<li class="hidden"></li>
<li class="hidden"></li>
<li class="hidden"></li>
<li class="hidden"></li>
<li class="hidden"></li>
<li class="hidden"></li>
<li class="hidden"></li>
<li class="hidden"></li>
</ul>
As soon as once more, for demo functions, I added a little bit of JavaScript that, this time, removes the .hidden
class of the following merchandise when clicking the addButton
and provides the hidden
class again when clicking the removeButton
:
doc.addEventListener("DOMContentLoaded", () => {
const addButton = doc.querySelector(".btn-add");
const removeButton = doc.querySelector(".btn-remove");
const listItems = doc.querySelectorAll('ul[role="list"] li');
let activeCount = 0;
addButton.addEventListener("click on", () => {
if (activeCount < listItems.size) {
listItems[activeCount].classList.take away("hidden");
activeCount++;
}
});
removeButton.addEventListener("click on", () => {
if (activeCount > 0) {
activeCount--;
listItems[activeCount].classList.add("hidden");
}
});
});
Let’s put collectively every part we realized to date, add a @starting-style
to our gadgets, and do the essential setup in CSS:
ul {
li {
show: block;
opacity: 1;
remodel: translate(0, 0);
transition: opacity 0.2s, remodel 0.2s;
@starting-style {
opacity: 0;
remodel: translate(0, -50%);
}
&.hidden {
show: none;
opacity: 0;
remodel: translate(0, 50%);
}
}
}
This time, we’ve got added the .hidden
class, set it to show: none
, and added the identical opacity
and remodel
declarations as we beforehand did with the .eradicating
class within the final instance. As you would possibly anticipate, we get a pleasant fade-in for our gadgets, however eradicating them continues to be very abrupt as we set our gadgets on to show: none
.
That is the place the transition-behavior
property comes into play. To interrupt it down a bit extra, let’s take away the transition
property shorthand of our earlier CSS and open it up a bit:
ul {
li {
show: block;
opacity: 1;
remodel: translate(0, 0);
transition-property: opacity, remodel;
transition-duration: 0.2s;
}
}
All that’s left to do is transition the show
property and set the transition-behavior
property to allow-discrete
:
ul {
li {
show: block;
opacity: 1;
remodel: translate(0, 0);
transition-property: opacity, remodel, show;
transition-duration: 0.2s;
transition-behavior: allow-discrete;
/* and so on. */
}
}
We are actually animating the component from show: none
, and the result’s precisely as we needed it:
We are able to use the transition
shorthand property to make our code rather less verbose:
transition: opacity 0.2s, remodel 0.2s, show 0.2s allow-discrete;
You’ll be able to add allow-discrete
in there. However in case you do, take notice that in case you declare a shorthand transition after transition-behavior
, it will likely be overruled. So, as a substitute of this:
transition-behavior: allow-discrete;
transition: opacity 0.2s, remodel 0.2s, show 0.2s;
…we wish to declare transition-behavior
after the transition
shorthand:
transition: opacity 0.2s, remodel 0.2s, show 0.2s;
transition-behavior: allow-discrete;
In any other case, the transition
shorthand property overrides transition-behavior
.
See the Pen [@starting-style and transition-behavior: allow-discrete [forked]](https://codepen.io/smashingmag/pen/GgKPXda) by utilitybend.
Animating Dialogs And Popovers Coming into And Exiting The High Layer
Let’s add just a few use circumstances with dialogs and popovers. Dialogs and popovers are good examples as a result of they get added to the highest layer when opening them.
What Is That High Layer?
We’ve already likened the “high layer” to a sibling of the <html>
component, however you may additionally consider it as a particular layer that sits above every part else on an internet web page. It’s like a clear sheet you can place over a drawing. Something you draw on that sheet can be seen on high of the unique drawing.
The unique drawing, on this instance, is the DOM. Which means the highest layer is out of the doc move, which gives us with just a few advantages. For instance, as I acknowledged earlier than, dialogs and popovers are added to this high layer, and that makes excellent sense as a result of they need to all the time be on high of every part else. No extra z-index: 9999
!
But it surely’s greater than that:
z-index
is irrelevant: Components on the highest layer are all the time on high, no matter theirz-index
worth.- DOM hierarchy doesn’t matter: A component’s place within the DOM doesn’t have an effect on its stacking order on the highest layer.
- Backdrops: We get entry to a brand new
::backdrop
pseudo-element that lets us type the world between the highest layer and the DOM beneath it.
Hopefully, you’re beginning to perceive the significance of the highest layer and the way we will transition components out and in of it as we’d with popovers and dialogues.
Transitioning The Dialog Component In The High Layer
The next HTML accommodates a button that opens a <dialog>
component, and that <dialog>
component accommodates one other button that closes the <dialog>
. So, we’ve got one button that opens the <dialog>
and one which closes it.
<button class="open-dialog" data-target="my-modal">Present dialog</button>
<dialog id="my-modal">
<p>Hello, there!</p>
<button class="define close-dialog" data-target="my-modal">
shut
</button>
</dialog>
Loads is occurring in HTML with invoker instructions that can make the next step a bit simpler, however for now, let’s add a little bit of JavaScript to make this modal truly work:
// Get all open dialog buttons.
const openButtons = doc.querySelectorAll(".open-dialog");
// Get all shut dialog buttons.
const closeButtons = doc.querySelectorAll(".close-dialog");
// Add click on occasion listeners to open buttons.
openButtons.forEach((button) =< {
button.addEventListener("click on", () =< {
const targetId = button.getAttribute("data-target");
const dialog = doc.getElementById(targetId);
if (dialog) {
dialog.showModal();
}
});
});
// Add click on occasion listeners to shut buttons.
closeButtons.forEach((button) =< {
button.addEventListener("click on", () =< {
const targetId = button.getAttribute("data-target");
const dialog = doc.getElementById(targetId);
if (dialog) {
dialog.shut();
}
});
});
I’m utilizing the next kinds as a place to begin. Discover how I’m styling the ::backdrop
as an added bonus!
dialog {
padding: 30px;
width: 100%;
max-width: 600px;
background: #fff;
border-radius: 8px;
border: 0;
box-shadow:
rgba(0, 0, 0, 0.3) 0px 19px 38px,
rgba(0, 0, 0, 0.22) 0px 15px 12px;
&::backdrop {
background-image: linear-gradient(
45deg in oklab,
oklch(80% 0.4 222) 0%,
oklch(35% 0.5 313) 100%
);
}
}
This ends in a fairly arduous transition for the entry, that means it’s not very clean:
Let’s add transitions to this dialog component and the backdrop. I’m going a bit sooner this time as a result of by now, you seemingly see the sample and know what’s occurring:
dialog {
opacity: 0;
translate: 0 30%;
transition-property: opacity, translate, show;
transition-duration: 0.8s;
transition-behavior: allow-discrete;
&[open] {
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 -30%;
}
}
}
When a dialog is open, the browser slaps an open
attribute on it:
<dialog open> ... </dialog>
And that’s one thing else we will goal with CSS, like dialog[open]
. So, on this case, we have to set a @starting-style
for when the dialog is in an open
state.
Let’s add a transition for our backdrop whereas we’re at it:
dialog {
/* and so on. */
&::backdrop {
opacity: 0;
transition-property: opacity;
transition-duration: 1s;
}
&[open] {
/* and so on. */
&::backdrop {
opacity: 0.8;
@starting-style {
opacity: 0;
}
}
}
}
Now you’re in all probability pondering: A-ha! However it’s best to have added the show
property and the transition-behavior: allow-discrete
on the backdrop!
However no, that isn’t the case. Even when I’d change my backdrop pseudo-element to the next CSS, the consequence would keep the identical:
&::backdrop {
opacity: 0;
transition-property: opacity, show;
transition-duration: 1s;
transition-behavior: allow-discrete;
}
It seems that we’re working with a ::backdrop
and when working with a ::backdrop
, we’re implicitly additionally working with the CSS overlay
property, which specifies whether or not a component showing within the high layer is at present rendered within the high layer.
And overlay
simply so occurs to be one other discrete property that we have to embody within the transition-property
declaration:
dialog {
/* and so on. */
&::backdrop {
transition-property: opacity, show, overlay;
/* and so on. */
}
Sadly, that is at present solely supported in Chromium browsers, however it may be completely used as a progressive enhancement.
And, sure, we have to add it to the dialog
kinds as properly:
dialog {
transition-property: opacity, translate, show, overlay;
/* and so on. */
&::backdrop {
transition-property: opacity, show, overlay;
/* and so on. */
}
See the Pen [Dialog: starting-style, transition-behavior, overlay [forked]](https://codepen.io/smashingmag/pen/pvzqOGe) by utilitybend.
It’s just about the identical factor for a popover as a substitute of a dialog. I’m utilizing the identical approach, solely working with popovers this time:
See the Pen [Popover transition with @starting-style [forked]](https://codepen.io/smashingmag/pen/emObLxe) by utilitybend.
Different Discrete Properties
There are just a few different discrete properties moreover those we lined right here. When you bear in mind the second demo, the place we transitioned some gadgets from and to show: none
, the identical will be achieved with the visibility
property as a substitute. This may be helpful for these circumstances the place you need gadgets to protect area for the component’s field, despite the fact that it’s invisible.
So, right here’s the identical instance, solely utilizing visibility
as a substitute of show
.
See the Pen [Transitioning the visibility property [forked]](https://codepen.io/smashingmag/pen/LEPMJqX) by utilitybend.
The CSS mix-blend-mode
property is one other one that’s thought of discrete. To be fully sincere, I can’t discover a good use case for a demo. However I went forward and created a considerably trite instance the place two mix-blend-mode
s swap proper in the course of the transition as a substitute of straight away.
See the Pen [Transitioning mix-blend-mode [forked]](https://codepen.io/smashingmag/pen/bNbOxZp) by utilitybend.
Wrapping Up
That’s an summary of how we will transition components out and in of the highest layer! In a great world, we may get away with no need a totally new property like transition-behavior
simply to transition in any other case “un-transitionable” properties, however right here we’re, and I’m glad we’ve got it.
However we additionally obtained to study @starting-style
and the way it gives browsers with a set of kinds that we will apply to the beginning of a transition for a component that’s within the high layer. In any other case, the component has nothing to transition from at first render, and we’d don’t have any solution to transition them easily out and in of the highest layer.

(gg, yk)