Design Systems Hot Takes

Out of order

7 min read

As someone who like to think he deeply understands the CSS I’m writing, I do everything I can to avoid using CSS that can cause unintended side effects. As an example, for my new personal website, I use z-index once to handle the page tearing animation. Trying to manipulate the order of things using CSS can seem reasonable at first but often has hidden problems that you might not notice. I’d like to outline some of those problems here.

Who’s on first?

Let’s say we have a dropdown component which produces some sort of flyout experience. We believe that setting some high z-index value will ensure that it is always on top of the content:

.dropdown {
    z-index: 100;
}

Then, when we create the modal component, we set it as a higher z-index because it is meant to be disruptive and on top of everything:

.modal {
    z-index: 200;
}

The question here is, what happens when the dropdown component is meant to be part of the modal composition? For example, perhaps you need to select from a large list of options within the modal. In the current setup, the flyout could appear underneath the modal because the z-index value for the dropdown is lower than the modal.

Now, when many folks find this problem, they start adding exceptions. Ok when this specific dropdown appears, we’re going to increase the z-index:

.dropdown.but-only-this-one {
    z-index: 300;
}

Sure, this’ll could work. But then when large teams are trying to use these components it turns into a priority war. With more things that are meant to appear on top of each other, it becomes less clear which items are more important at which times. Everyone thinks their use is the exception to the rule and then no system truly exists.

Some design systems took Material’s guidelines for elevation and turned them into design tokens for z-index. Unfortunately, based on my example above, this doesn’t work. It’s possible that your tooltip is meant to appear in a modal, causing it’s z-index number to increase.

So what’s the solution? Avoid using z-index altogether.

When you start working with the position property, and then try reordering the elements, you should notice a certain quality. Having nothing else set, the order that the elements appear matches the DOM order. In other words, later elements will appear on top of elements that come before. In the following example, try rearranging the HTML elements without touching the CSS.

Notice that whatever box is rendered last, is the box that appears on top of the others. This means if we want things to appear on top of other things, we should be rendering them last in the DOM order.

In fact, this is exactly how the new top-layer works in the DOM. The top-layer is exactly what it sounds like, it is a layer that exists on top of everything else in the page. That means even trying to set z-index: Infinity will not work here. Speaking of z-index, that doesn’t work on elements in this layer either. The only way to affect the order of elements here is by DOM order. The last rendered element in the layer is the element that appears on top. If you want something to be on top of that, you’ll need to append it to the layer.

Inset fill

But, let’s say you don’t need to do any flyouts or disruptions (good!). You want to do something more simple like have an element fill a container without itself contributing to the size of the container. Traditionally, we’d use the inset fill technique to do this.

.parent {
    position: relative;
}

.child {
    position: absolute;
    inset: 0; /* same as setting top, left, bottom, right all to 0 */
}

This was very common when we needed maintain the aspect ratio for media by using the padding hack. While we don’t need that specific hack any longer, there’s still use-cases for making an element fill a parent container. In those cases, I still recommend avoiding the use of z-index by making the element to appear on top last in the DOM order. But I’d also recommend avoiding the position property altogether.

Josh Comeau has a great post about stacking contexts that you should absolutely read. But the tl;dr is that using position creates a stacking context. This is good for when you want to use z-index but bad when other stacking contexts are created elsewhere and conflict in some way. Most people don’t know the properties that create stacking contexts, but these are usually the culprits to when you think you need to start introducing z-index into your CSS. Here’s a few important points:

This illustrates one of the things that makes people upset about CSS. There are properties that can affect other things in the composition in ways you don’t expect. You’d think that simply setting opacity: .5; is just making the element translucent, but there’s other side effects associated with it that could be unintended. Unfortunately, the only way to really know these is with experience. The more you avoid learning this, the more you’ll wonder what is happening every moment you begrudgingly write CSS.

Getting back to the inset fill technique. Instead of using position, I recommend trying to use display: grid; instead:

.parent {
    display: grid; /** or inline-grid */
}

.child {
    grid-area: 1 / 1 / -1 / -1;
}

The grid-area set in the child says to make sure this item touches all of the edges of the parent grid, filling the space. This forces the element to appear on top of any other children that may be within the grid.

Reading flow

Ordering things in the third dimension isn’t the only way we can affect elements using CSS. We’ve had the order property since the introduction of Flexbox. This has the ability to visually rearrange the elements in a composition without changing their order in the DOM.

.flex-child {
    order: 2;
}

There’s other properties that can change the visual order, such as flex-direction and even grid-areas. However, my recommendation is to avoid doing this, and if you must change the order to update the DOM order instead.

Now I know what you’re probably thinking, using JavaScript to rearrange the DOM sounds expensive when CSS would be much more performant and that’s true. The problem comes down to accessibility. The page structure should match the visual order of elements. If it doesn’t, then folks who can’t see the visual rearrangement can be confused about the order of the elements.

Imagine that someone who uses an assistive technology has called a help line for assistance with their account. The person on the phone says, “click the first button”. If the DOM order doesn’t match the visual order, the first button could be different to these folks and cause confusion down the line.

I keep this in mind when designing, and consider the ways I could keep the order maintained when the layout needs to change based on the available size. Sometimes though, you get a design in your inbox that can’t have reordering avoided. That’s when as of right now I’d recommend using a ResizeObserver to reflow the elements instead of CSS for the reasons I’ve described above. Here’s an implementation for my design system playground.

Bring order

Good things are on the way! There’s a new CSS property being discussed called reading-flow which is designed to help the element order problems I’ve described above. If this property becomes available, then we could tell the browser what order the elements should be in if the visual order doesn’t match. Rachel Andrew has some examples and a presentation on this. This is relating directly to the Masonry discussions to help understand what the intended flow is meant to be for the user. Having this along with top-layer will make setting the order of elements more clear than ever before.

Bottom line is that when you are trying to update the order of elements using CSS, pause to think of the side-effects of these decisions and refer to some of the recommendations here to help keep a consistent structure.

Less Gridless