Tuesday, February 11, 2025
HomeWeb developmentTransitioning High-Layer Entries And The Show Property In CSS — Smashing Journal

Transitioning High-Layer Entries And The Show Property In CSS — Smashing Journal


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.

See the Pen @starting-style demo – up-in, down-out [forked] 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.

See the Pen @starting-style and transition-behavior: allow-discrete [forked] 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 their z-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.

See the Pen Dialog: starting-style, transition-behavior, overlay [forked] 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.

See the Pen Popover transition with @starting-style [forked] 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.

See the Pen Transitioning the visibility property [forked] 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-modes 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.

See the Pen Transitioning mix-blend-mode [forked] 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.

Smashing Editorial
(gg, yk)
RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments