Transitioning Top-Layer Entries And The Display Property In CSS
smashingmagazine.com
Animating from and to display: none; was something we could only achieve with JavaScript to change classes or create other hacks. The reason why we couldnt do this in CSS is explained in the new CSS Transitions Level 2 specification:In Level 1 of this specification, transitions can only start during a style change event for elements that have a defined before-change style established by the previous style change event. That means a transition could not be started on an element that was not being rendered for the previous style change event.In simple terms, this means that we couldnt start a transition on an element that is hidden or that has just been created.What Does transition-behavior: allow-discrete Do?allow-discrete is a bit of a strange name for a CSS property value, right? We are going on about transitioning display: none, so why isnt this named transition-behavior: allow-display instead? The reason is that this does a bit more than handling the CSS display property, as there are other discrete properties in CSS. A simple rule of thumb is that discrete properties do not transition but usually flip right away between two states. Other examples of discrete properties are visibility and mix-blend-mode. Ill include an example of these at the end of this article.To summarise, setting the transition-behavior property to allow-discrete allows us to tell the browser it can swap the values of a discrete property (e.g., display, visibility, and mix-blend-mode) at the 50% mark instead of the 0% mark of a transition.What Does @starting-style Do?The @starting-style rule defines the styles of an element right before it is rendered to the page. This is highly needed in combination with transition-behavior and this is why:When an item is added to the DOM or is initially set to display: none, it needs some sort of starting style from which it needs to transition. To take the example further, popovers and dialog elements are added to a top layer which is a layer that is outside of your document flow, you can kind of look at it as a sibling of the <html> element in your pages structure. Now, when opening this dialog or popover, they get created inside that top layer, so they dont have any styles to start transitioning from, which is why we set @starting-style. Dont worry if all of this sounds a bit confusing. The demos might make it more clearly. The important thing to know is that we can give the browser something to start the animation with since it otherwise has nothing to animate from.A Note On Browser SupportAt the moment of writing, the transition-behavior is available in Chrome, Edge, Safari, and Firefox. Its the same for @starting-style, but Firefox currently does not support animating from display: none. But remember that everything in this article can be perfectly used as a progressive enhancement.Now that we have the theory of all this behind us, lets get practical. Ill be covering three use cases in this article:Animating from and to display: none in the DOM.Animating dialogs and popovers entering and exiting the top layer.More discrete properties we can handle.Animating From And To display: none In The DOMFor the first example, lets take a look at @starting-style alone. I created this demo purely to explain the magic. Imagine you want two buttons on a page to add or remove list items inside of an unordered list.This could be your starting HTML:<button type="button" class="btn-add"> Add item</button><button type="button" class="btn-remove"> Remove item</button><ul role="list"></ul>Next, we add actions that add or remove those list items. This can be any method of your choosing, but for demo purposes, I quickly wrote a bit of JavaScript for it:document.addEventListener("DOMContentLoaded", () => { const addButton = document.querySelector(".btn-add"); const removeButton = document.querySelector(".btn-remove"); const list = document.querySelector('ul[role="list"]'); addButton.addEventListener("click", () => { const newItem = document.createElement("li"); list.appendChild(newItem); }); removeButton.addEventListener("click", () => { if (list.lastElementChild) { list.lastElementChild.classList.add("removing"); setTimeout(() => { list.removeChild(list.lastElementChild); }, 200); } });});When clicking the addButton, an empty list item gets created inside of the unordered list. When clicking the removeButton, the last item gets a new .removing class and finally gets taken out of the DOM after 200ms.With this in place, we can write some CSS for our items to animate the removing part:ul { li { transition: opacity 0.2s, transform 0.2s; &.removing { opacity: 0; transform: translate(0, 50%); } } }This is great! Our .removing animation is already looking perfect, but what we were looking for here was a way to animate the entry of items coming inside of our DOM. For this, we will need to define those starting styles, as well as the final state of our list items.First, lets update the CSS to have the final state inside of that list item:ul { li { opacity: 1; transform: translate(0, 0); transition: opacity 0.2s, transform 0.2s; &.removing { opacity: 0; transform: translate(0, 50%); } } }Not much has changed, but now its up to us to let the browser know what the starting styles should be. We could set this the same way we did the .removing styles like so:ul { li { opacity: 1; transform: translate(0, 0); transition: opacity 0.2s, transform 0.2s; @starting-style { opacity: 0; transform: translate(0, 50%); } &.removing { opacity: 0; transform: translate(0, 50%); } } }Now weve let the browser know that the @starting-style should include zero opacity and be slightly nudged to the bottom using a transform. The final result is something like this:But we dont need to stop there! We could use different animations for entering and exiting. We could, for example, update our starting style to the following:@starting-style { opacity: 0; transform: translate(0, -50%);}Doing this, the items will enter from the top and exit to the bottom. See the full example in this CodePen:See the Pen @starting-style demo - up-in, down-out [forked] by utilitybend.When To Use transition-behavior: allow-discreteIn the previous example, we added and removed items from our DOM. In the next demo, we will show and hide items using the CSS display property. The basic setup is pretty much the same, except we will add eight list items to our DOM with the .hidden class attached to it: <button type="button" class="btn-add"> Show item </button> <button type="button" class="btn-remove"> Hide item </button><ul role="list"> <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>Once again, for demo purposes, I added a bit of JavaScript that, this time, removes the .hidden class of the next item when clicking the addButton and adds the hidden class back when clicking the removeButton:document.addEventListener("DOMContentLoaded", () => { const addButton = document.querySelector(".btn-add"); const removeButton = document.querySelector(".btn-remove"); const listItems = document.querySelectorAll('ul[role="list"] li'); let activeCount = 0; addButton.addEventListener("click", () => { if (activeCount < listItems.length) { listItems[activeCount].classList.remove("hidden"); activeCount++; } }); removeButton.addEventListener("click", () => { if (activeCount > 0) { activeCount--; listItems[activeCount].classList.add("hidden"); } });});Lets put together everything we learned so far, add a @starting-style to our items, and do the basic setup in CSS:ul { li { display: block; opacity: 1; transform: translate(0, 0); transition: opacity 0.2s, transform 0.2s; @starting-style { opacity: 0; transform: translate(0, -50%); } &.hidden { display: none; opacity: 0; transform: translate(0, 50%); } } }This time, we have added the .hidden class, set it to display: none, and added the same opacity and transform declarations as we previously did with the .removing class in the last example. As you might expect, we get a nice fade-in for our items, but removing them is still very abrupt as we set our items directly to display: none.This is where the transition-behavior property comes into play. To break it down a bit more, lets remove the transition property shorthand of our previous CSS and open it up a bit:ul { li { display: block; opacity: 1; transform: translate(0, 0); transition-property: opacity, transform; transition-duration: 0.2s; } }All that is left to do is transition the display property and set the transition-behavior property to allow-discrete:ul { li { display: block; opacity: 1; transform: translate(0, 0); transition-property: opacity, transform, display; transition-duration: 0.2s; transition-behavior: allow-discrete; /* etc. */ } }We are now animating the element from display: none, and the result is exactly as we wanted it:We can use the transition shorthand property to make our code a little less verbose:transition: opacity 0.2s, transform 0.2s, display 0.2s allow-discrete;You can add allow-discrete in there. But if you do, take note that if you declare a shorthand transition after transition-behavior, it will be overruled. So, instead of this:transition-behavior: allow-discrete;transition: opacity 0.2s, transform 0.2s, display 0.2s;we want to declare transition-behavior after the transition shorthand:transition: opacity 0.2s, transform 0.2s, display 0.2s;transition-behavior: allow-discrete;Otherwise, the transition shorthand property overrides transition-behavior.See the Pen @starting-style and transition-behavior: allow-discrete [forked] by utilitybend.Animating Dialogs And Popovers Entering And Exiting The Top LayerLets add a few use cases with dialogs and popovers. Dialogs and popovers are good examples because they get added to the top layer when opening them.What Is That Top Layer?Weve already likened the top layer to a sibling of the <html> element, but you might also think of it as a special layer that sits above everything else on a web page. It's like a transparent sheet that you can place over a drawing. Anything you draw on that sheet will be visible on top of the original drawing.The original drawing, in this example, is the DOM. This means that the top layer is out of the document flow, which provides us with a few benefits. For example, as I stated before, dialogs and popovers are added to this top layer, and that makes perfect sense because they should always be on top of everything else. No more z-index: 9999! But its more than that:z-index is irrelevant: Elements on the top layer are always on top, regardless of their z-index value.DOM hierarchy doesnt matter: An elements position in the DOM doesnt affect its stacking order on the top layer.Backdrops: We get access to a new ::backdrop pseudo-element that lets us style the area between the top layer and the DOM beneath it.Hopefully, you are starting to understand the importance of the top layer and how we can transition elements in and out of it as we would with popovers and dialogues.Transitioning The Dialog Element In The Top LayerThe following HTML contains a button that opens a <dialog> element, and that <dialog> element contains another button that closes the <dialog>. So, we have one button that opens the <dialog> and one that closes it.<button class="open-dialog" data-target="my-modal">Show dialog</button><dialog id="my-modal"> <p>Hi, there!</p> <button class="outline close-dialog" data-target="my-modal"> close </button></dialog>A lot is happening in HTML with invoker commands that will make the following step a bit easier, but for now, lets add a bit of JavaScript to make this modal actually work:// Get all open dialog buttons.const openButtons = document.querySelectorAll(".open-dialog");// Get all close dialog buttons.const closeButtons = document.querySelectorAll(".close-dialog");// Add click event listeners to open buttons.openButtons.forEach((button) =< { button.addEventListener("click", () =< { const targetId = button.getAttribute("data-target"); const dialog = document.getElementById(targetId); if (dialog) { dialog.showModal(); } });});// Add click event listeners to close buttons.closeButtons.forEach((button) =< { button.addEventListener("click", () =< { const targetId = button.getAttribute("data-target"); const dialog = document.getElementById(targetId); if (dialog) { dialog.close(); } });});Im using the following styles as a starting point. Notice how Im 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 results in a pretty hard transition for the entry, meaning its not very smooth:Lets add transitions to this dialog element and the backdrop. Im going a bit faster this time because by now, you likely see the pattern and know whats happening:dialog { opacity: 0; translate: 0 30%; transition-property: opacity, translate, display; 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 thats something else we can target with CSS, like dialog[open]. So, in this case, we need to set a @starting-style for when the dialog is in an open state.Lets add a transition for our backdrop while were at it:dialog { /* etc. */ &::backdrop { opacity: 0; transition-property: opacity; transition-duration: 1s; } &[open] { /* etc. */ &::backdrop { opacity: 0.8; @starting-style { opacity: 0; } } }}Now youre probably thinking: A-ha! But you should have added the display property and the transition-behavior: allow-discrete on the backdrop!But no, that is not the case. Even if I would change my backdrop pseudo-element to the following CSS, the result would stay the same: &::backdrop { opacity: 0; transition-property: opacity, display; transition-duration: 1s; transition-behavior: allow-discrete; }It turns out that we are working with a ::backdrop and when working with a ::backdrop, were implicitly also working with the CSS overlay property, which specifies whether an element appearing in the top layer is currently rendered in the top layer. And overlay just so happens to be another discrete property that we need to include in the transition-property declaration:dialog { /* etc. */&::backdrop { transition-property: opacity, display, overlay; /* etc. */}Unfortunately, this is currently only supported in Chromium browsers, but it can be perfectly used as a progressive enhancement.And, yes, we need to add it to the dialog styles as well:dialog { transition-property: opacity, translate, display, overlay; /* etc. */&::backdrop { transition-property: opacity, display, overlay; /* etc. */}See the Pen Dialog: starting-style, transition-behavior, overlay [forked] by utilitybend.Its pretty much the same thing for a popover instead of a dialog. Im using the same technique, only working with popovers this time:See the Pen Popover transition with @starting-style [forked] by utilitybend.Other Discrete PropertiesThere are a few other discrete properties besides the ones we covered here. If you remember the second demo, where we transitioned some items from and to display: none, the same can be achieved with the visibility property instead. This can be handy for those cases where you want items to preserve space for the elements box, even though it is invisible.So, heres the same example, only using visibility instead of display.See the Pen Transitioning the visibility property [forked] by utilitybend.The CSS mix-blend-mode property is another one that is considered discrete. To be completely honest, I cant find a good use case for a demo. But I went ahead and created a somewhat trite example where two mix-blend-modes switch right in the middle of the transition instead of right away.See the Pen Transitioning mix-blend-mode [forked] by utilitybend.Wrapping UpThats an overview of how we can transition elements in and out of the top layer! In an ideal world, we could get away without needing a completely new property like transition-behavior just to transition otherwise un-transitionable properties, but here we are, and Im glad we have it.But we also got to learn about @starting-style and how it provides browsers with a set of styles that we can apply to the start of a transition for an element thats in the top layer. Otherwise, the element has nothing to transition from at first render, and wed have no way to transition them smoothly in and out of the top layer.
0 Comments
·0 Shares
·27 Views