![Noam Rosenthal](https://web.dev/images/authors/nrosenthal.jpg) Noam Rosenthal [X](https://twitter.com/nomsternom) [GitHub](https://github.com/noamr) [Mastodon](https://indieweb.social/@nomster)

<br />

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 unregistered custom property declaration | `var()` |
| View transitions | `view-transition-name` `view-transition-class` | `::view-transition-*` pseudo elements |
| Anchor Positioning | `anchor-name` | `position-anchor` |
| Scroll-driven animation | `view-timeline-name` `scroll-timeline-name` | `animation-timeline` |
| List styles | `@counter-style` | `list-style` |
| Counters | `counter-reset` `counter-set` `counter-increment` ||
| Container queries | `container-name` | `@container` |
| Page | `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](https://web.dev/articles/shadowdom-v1) 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](https://drafts.csswg.org/css-scoping-1/#shadow-names).
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`](https://developer.mozilla.org/docs/Web/CSS/@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](https://developer.mozilla.org/docs/Web/CSS/CSS_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](https://github.com/w3c/csswg-drafts/issues/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](https://github.com/w3c/csswg-drafts/issues/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.