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 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:
- First, check the style inside the shadow context. This is the "default" style of the part.
- Then, apply the external style as defined in
::part
. This is the "customized" style of the part. - 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
andview-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.