Author-defined CSS names and shadow DOM: In specification and in practice

Authored-defined CSS names and the shadow DOM are supposed to work together. However, browsers are inconsistent with the specification, sometimes with each other, and every CSS name is inconsistent in a slightly different way.

This article documents the current status of how author-defined CSS names behave across shadow scopes, with the hope that it can serve as a guide to improve interoperability in the near future.

What are author-defined CSS names?

Author-defined CSS names are a relatively old CSS syntax mechanism, originally introduced for the @keyframes rule, which defines a <keyframe-name> as either a custom-ident or a string. The purpose of this concept is to declare something in one part of a stylesheet, and refer to it in another part.

/* "fade-in" is a CSS name, representing a set of keyframes */
@keyframes fade-in {
  from { opacity: 0 };
  to { opacity: 1 }
}

.card {
  /* "fade-in" is a reference to the above keyframes */
  animation-name: fade-in;
}

Other CSS features that use CSS names are fonts, property declarations, container queries, and more recently view transitions, anchor positioning and scroll-driven animations. The following non-comprehensive table includes names that Chrome checks the state of.

Feature Name declaration Name reference
Keyframes @keyframes animation-name
Fonts @font-face { }
@font-palette-values
font-family
font-palette
Property Declarations @property Any custom property
View transition view-transition-name
view-transition-class
::view-transition-group()
Anchor Positioning anchor-name position-anchor
Scroll-driven animation animation-timeline view-timeline-name
scroll-timeline-name
Counter style @counter-style
Counter-reset
counter-set
counter-increment
list-style
Container queries container-name @container
CSS variable --something var(--something)
Page @page

As you can see in the table, a CSS name usually has a corresponding CSS reference. For example, animation-name is a reference to the @keyframes name. CSS names are different from names defined in the DOM, such as attributes and tag names, as they are declared then referenced within the context of stylesheets.

How names relate to the shadow DOM

While CSS names are built to create relationships between different parts of a document or stylesheet, Shadow DOM is built to do the opposite. It encapsulates relationships so they don't leak across web components that are supposed to have their own namespace.

By bringing CSS names and the shadow DOM together, the experience of composing web components should feel expressive enough to be flexible but constrained enough to be stable.

This is good in theory. In practice, browsers are inconsistent in the way CSS names interoperate with the shadow DOM, both between features in the same browser, across browsers, and between the features and the specification.

How names and the shadow DOM should work together

To understand the problem, it's worth understanding how these parts of CSS should work together in theory.

The general rule

The general rule for how CSS names behave across shadow trees is defined in the CSS Scoping Level 1 specification. To summarize: a CSS name is global inside the scope in which it is defined, meaning it can be accessed from descendant shadow trees, but not from sibling or ancestor shadow trees. Note that this is unlike names in the web platform like element IDs, which are encapsulated within the same tree scope.

Exception to the rule: @property

Unlike other CSS names, CSS properties are not encapsulated by shadow DOM. Rather, they are the common means to pass parameters across different shadow trees. This makes the @property descriptor special: it's supposed to behave like a document-global type declaration that defines how a particular named property acts. Because properties have to match across shadow trees, mismatch of property declarations would create unexpected results, so @property declarations are specified to be flattened and resolved according to document order.

How the rule should work with ::part

Shadow parts expose an element inside a shadow tree to its parent tree. By doing so, the parent tree can access that element and also style it using the ::part element.

Since ::part allows two tree scopes to style the same element, the following cascade order is specified:

  1. First, check the style inside the shadow context. This is the "default" style of the part.
  2. Then, apply the external style as defined in ::part. This is the "customized" style of the part.
  3. Then, apply any internal style that's defined together with !important. This allows a custom element to declare that a certain property of a certain part is not customizable by ::part.

This means that names from within the shadow DOM cannot be referenced from a ::part, as the ::part is a host-scoped style rather than a shadow-scoped style. For example:

// inside the shadow DOM:
@keyframes fade-in {
  from { opacity: 0}
}

// This shouldn't work!
// The host style shouldn't know the name "fade-in"
::part(slider) {
  animation-name: fade-in;  
}

How the rule should work with inline styles

Unlike ::part, inline styles with the style attribute, or those programmatically setting the style using script, are scoped to where the element is scoped to. That's because to apply a style to an element you need access to the element handle, and thus to the shadow root itself.

How CSS names and the shadow DOM work together in reality

Though the preceding rules are well-defined and consistent, the current implementations don't always reflect that. In practice, @property works differently from the spec in a consistent way across browsers, and most of the other features have open bugs (some of them are not yet released, so there's time to fix them).

To test and demonstrate how these features work in practice, we've created the following page: https://css-names-in-the-shadow.glitch.me/. This page has several iframes, each focused on one of the features and testing six scenarios:

  • Outer reference to an outer name: no shadow DOM involved, this should work.
  • Outer reference to an inner name: this shouldn't work, as that would mean that the name defined in the shadow context has leaked.
  • Inner reference to outer name: this should work, as tree-scoped names are inherited by shadow roots.
  • Inner reference to inner name: this should work, as both the name of the reference are in the same scope.
  • ::part reference to outer name: this should work, as both the ::part and the name are declared in the same scope.
  • ::part reference to inner name: this shouldn't work, as the outer scope shouldn't gain knowledge about names declared inside the shadow DOM.

@keyframes

As defined in the specification, you should be able to reference keyframe names from within a shadow root, as long as the @keyframes at-rule is in an ancestor scope. In practice, no browser implements this behavior, and the keyframe definitions can only be referenced in the scope in which they're defined. See issue 10540.

@property

As defined in the specification, any declaration of @property will be flattened to the document scope. Today however, in all browsers you can only declare @property in the document scope and @property declarations within shadow roots are ignored.
See issue 10541.

Browser specific bugs

The other features don't show consistent behavior across browsers:

  • @font-face is flattened to the root scope in Safari.
  • Chromium doesn't allow inheriting @anchor-name rules in a shadow root
  • @scroll-timeline-name and @view-timeline-name are not scoped correctly on ::part (also in Chromium).
  • No browser allows declaring @font-palette-values in a shadow roots.
  • view-transition-class can be defined inside a shadow root (the transition itself is outside the shadow-root).
  • Firefox lets ::part access inner shadow names (container queries, keyframes).
  • Firefox and Safari don't respect @counter-style in a shadow root.

Note that counter-reset, counter-set, counter-increment have slightly different rules because they're implicit names, and declaring CSS properties have an established and well tested set of rules.

Conclusion

The bad news is that when examining the snapshot of the current interop state with regards to CSS names and the shadow DOM, the experience is inconsistent and buggy. None of the features we examined here behaves consistently across browsers and according to spec. The good news is that the delta to make the experience consistent is a finite list of bugs and spec issues. Let's fix this! In the meantime, this overview can hopefully help if you are struggling with the inconsistencies described in this article.