Mayank RSS

Some use cases for revert-layer

Did you know about the revert-layer keyword? It belongs in the same family as other CSS-wide global values, namely inherit, initial, unset, revert; all of which can be used on any (and all) properties.

revert-layer is the only part of cascade layers that cannot be easily polyfilled. This might have deterred some developers from using it in the past. However I think it has a reached a state where it’s good enough for widespread use. revert-layer has worked fine in the three major browser engines ever since their impressive coordinated rollout of cascade layers back in February 2022 (See caniuse).

Update: The headings on this page are suffixed with “reset”, which I now realize can potentially be confused with a CSS reset. I do explicitly mention CSS resets later in the article, but that’s not what I’m talking about in general when I use the word “reset”. Think of it more like a synonym for “revert”.

The nuclear reset

If you’ve ever tried using all: revert, then you’re probably aware of what a disappointment it is. On paper it gives you a clean slate, but in practice it does too much. It gets rid of (desirable!) styles that come with draggable, contenteditable, svg attributes, image width/height attributes, etc. (I think these are called “presentational hints”?)

Anyway, all: revert-layer is like all: revert but actually useful. It can be used as a drop-in replacement for all: revert, even if you are not making use of cascade layers anywhere else. When there is no “previous layer”, revert-layer will revert to the previous origin. But unlike revert which removes the useful styles associated with presentational attributes, revert-layer will correctly preserve them.

One recurring use I’ve found for this is when I want to isolate a portion of the page (e.g. for demos, previews or third-party components), preventing document styles from leaking in.

Code snippet
my-demo {
  &,
  * {
    all: revert-layer;
  }
}

In many ways, all: revert-layer offers better encapsulation than shadow DOM, because it will reset everything to the browser defaults, even inherited typographic properties. Use with caution, but confidently!

Do note that if you have too many layers, you will need to revert them all one by one. (I don’t think this is a bad thing; I actually like that it forces you to be more intentional about your layers and sublayers, so you don’t end up with stray top-level layers).

Code snippet
my-demo {
  &,
  * {
    @layer reset {
      all: revert-layer;
    }
    @layer globals {
      all: revert-layer;
    }
    @layer components {
      all: revert-layer;
    }
    @layer pages {
      all: revert-layer;
    }
  }
}

The surgical reset

Building components using cascade layers opens up the possibility of conditionally resetting some properties.

For example, your reset might remove the default list styles, which is fine for 99% cases. But for prose content, you actually want those list styles. You can easily bring them back! revert-layer is particularly great for these “edge case”-y scenarios.

Code snippet
.prose :is(ul, ol) {
  @layer reset {
    list-style: revert-layer;
  }
}

Another example: you can revert a layer based on multiple conditions without duplication. You might implement a high-contrast mode on your website, controlled using not just the prefers-contrast query but also a custom toggle. It’s kinda like runtime mixins?

Code snippet
@media not (prefers-contrast: more) {
  html:not([data-contrast]) * {
    @layer components.highcontrast {
      all: revert-layer;
    }
  }
}
html[data-contrast="low"] * {
  @layer components.highcontrast {
    all: revert-layer;
  }
}

The beauty of this technique is that you get to control exactly “how much” of the cascade to revert (by specifying layers and property names) and “when” to revert it (by specifying selectors and queries).

The versioned reset

Let’s say you have a .button class used in a million different places, and now it’s time for a redesign.

One way of going about this is by creating a .button-new class, but then all the markup needs to be updated. This is not always feasible, especially when multiple teams are involved.

Layers provide a natural way of versioning CSS, enabling seamless incremental migrations without causing cascade wars.

You can put all your old styles in a v1 layer. Heck, this layer could even be named legacy if you don’t care about actual versioning. Point is, this layer can be easily reverted for a specific sub-tree.

Code snippet
.new-ui * {
  @layer versions.v1 {
    all: revert-layer;
  }
}

Realistically, that selector will need to be adjusted to consider generated content. Its specificity will also need to be artifically inflated (using the impossible id selector), so that it takes precedence over all other selectors in that layer. It might look hacky, but it works quite well!

Code snippet
.new-ui *:not(#a#b) {
  &,
  &::before,
  &::after {
    @layer versions.v1 {
      all: revert-layer;
    }
  }
}

Important note: do not use revert-layer !important here. It is not a substitute for increasing specificity; it will actually prevent any subsequent layers’ styles from being applied, which is likely not what you want if you’re versioning your CSS.

The self-reset

You know how using a custom property overrides any previous declarations even if the custom property doesn’t have a value?

Code snippet
svg {
  @layer base {
    width: 1rem;
    height: 1rem;
  }

  /* These will throw away the base width/height,
     even if --size is undefined :( */
  width: var(--size);
  height: var(--size);

  fill: var(--fill);
}

To get around this, you can specify a fallback value for the custom property (maybe even combined with pseudo-private custom properties), but that usually involves restructuring/rewriting your code. And depending on which layer you use it in, it can still end up incorrectly throwing away valid declarations from lower-priority layers.

Here’s the trick: you can specify revert-layer as the fallback! If the custom property never gets set, it will be automatically reverted, as if it never existed in the first place. Declarations from lower layers will continue to work.

Code snippet
width: var(--size, revert-layer);
height: var(--size, revert-layer);
fill: var(--fill, revert-layer);

CSS is a proper programming language, alright!

Update: Roman Komarov has combined this technique with his amazing cyclic toggles technique to emulate mixins. He’s calling it layered toggles. Go read it!

The un-!important reset

The role of !important has evolved from being a shameful hack to being a robust technique for managing priorities. Instead of attempting to poorly explain how the cascade works, I’m going to suggest you read Miriam’s excellent guide to cascade layers if you haven’t already. The gist of it is that !important styles from lower layers will always take priority over higher layers. This might feel counterintuitive at first, but it will make sense I promise!

I’ve started using !important in quite a few places now, because revert-layer makes it so simple to revert them later.

Consider these three parts of a CSS reset:

  1. Reinforced hidden attribute and safeguards for dialog/popover.

    Code snippet
    @layer reset {
      :where([hidden]:not([hidden="until-found"])) {
        display: none !important;
      }
      :where(dialog:not([open]), [popover]:not(:popover-open)) {
        display: none !important;
      }
    }
  2. The visually-hidden ruleset (which should just be built into CSS at this point btw).

    Code snippet
    @layer reset.visually-hidden {
      :where(.visually-hidden:not(:focus-within, :active)) {
        clip-path: inset(50%) !important;
        height: 1px !important;
        width: 1px !important;
        overflow: hidden !important;
        position: absolute !important;
        white-space: nowrap !important;
      }
    }
  3. The universal focus indicator.

    Code snippet
    @layer reset {
      :where(:focus-visible) {
        outline: 3px solid CanvasText !important;
        box-shadow: 0 0 0 6px Canvas;
      }
    }

Using !important in all of these cases prevents the possibility of some higher layer potentially messing up my carefully orchestrated resets and utility classes.

When there are legitimate reasons for reverting these styles, revert-layer provides a way to safely add exceptions, avoiding any specificity battles that would traditionally result from the use of !important. It’s like saying “Sure, this declaration may be generally important, but it is no longer important for this particular case”.

What legitimate reasons might I have for reverting those three parts of my CSS reset?

  1. If I need more control over the display property, for animation reasons or otherwise.

    Code snippet
    .animated-dialog {
      @layer reset {
        display: revert-layer;
      }
      visibility: hidden;
    }
  2. If I need to “unhide” a visually-hidden element when a parent element is hovered or has focus. (Think of a card which might have other interactable elements).

    Code snippet
    .my-card:is(:hover, :focus-within) {
      .my-button:is(.visually-hidden) {
        @layer reset.visually-hidden {
          all: revert-layer;
        }
      }
    }
  3. If I need a custom focus indicator for some special control.

    Code snippet
    .my-component:focus-visible {
      @layer reset {
        outline: revert-layer;
      }
      /* … alternative focus styles, presumably */
    }

I find that this approach feels way more intentional than “Oh I just slapped an !important on here because my selector is too weak”. Perhaps more importantly, it opens up interesting new capabilities that I’ve found difficult to achieve before (such as reverting the visually-hidden styles, as described above).

Bonus: the super-nuclear reset

Lastly, I want to give a shoutout to Nathan Knowler’s post about shadow-less style encapsulation, where he describes how you can use all: revert-layer !important to achieve ShadowDOM-like encapsulation. Go read it!

Code snippet
@layer components {
  .icon-button {
    @layer {
      color: hotpink;
      /* … all scoped styles go in this nested layer */
    }

    /* This prevents "outside" styles from bleeding in,
       revert-layer resets everything to the previous sublayer,
       and !important overrides styles from subsequent layers. */
    all: revert-layer !important;
  }
}