CSS :state() Pseudo-Class in Custom Web Components
This article explores the CSS :state() pseudo-class, a
feature of the Custom State Set API that allows developers to style
autonomous custom web components based on their internal, custom states.
By reading this guide, you will learn why :state() was
introduced, how it replaces fragile attribute- and class-based patterns,
and how to implement it directly within JavaScript and CSS to achieve
clean encapsulation.
What is the
:state() Pseudo-Class?
The :state() pseudo-class represents custom internal
states exposed by a custom element. Historically, native HTML elements
have built-in pseudo-classes to reflect their interactive states, such
as :checked for checkboxes, :disabled for
buttons, or :hover.
Before the introduction of the Custom State API, custom elements
could not define their own pseudo-classes. Instead, developers had to
use custom HTML attributes (e.g., loading="true") or
utility classes (e.g., class="is-loading"). The CSS
:state() pseudo-class addresses this limitation by allowing
custom components to expose internal states directly to CSS selectors,
behaving identically to built-in pseudo-classes.
The Problem It Solves
Using attributes or classes to track internal component state introduces several problems:
- DOM Pollution: State-tracking attributes clutter the DOM and expose implementation details.
- Encapsulation Leaks: Users of the component might accidentally remove or manipulate classes or attributes that the component relies on to function.
- Selector Specificity Issues: Class- and attribute-based selectors increase specificity unpredictably compared to native pseudo-classes.
The :state() pseudo-class solves this by separating
private internal state management from the public DOM tree.
How the Custom State API Works
The feature relies on two key parts: the CustomStateSet
interface in JavaScript and the :state() selector in
CSS.
1. Managing States in JavaScript
To expose a state, an autonomous custom element calls
this.attachInternals() to get an
ElementInternals object. This object contains a
states property, which is an instance of
CustomStateSet—a Set-like collection of
strings representing active states.
class ToggleSwitch extends HTMLElement {
constructor() {
super();
this.internals = this.attachInternals();
this.addEventListener('click', this._toggle);
}
_toggle() {
if (this.internals.states.has('checked')) {
this.internals.states.delete('checked');
} else {
this.internals.states.add('checked');
}
}
}
customElements.define('toggle-switch', ToggleSwitch);Whenever this.internals.states.add('checked') is called,
the element enters that state. Calling
this.internals.states.delete('checked') exits it.
2. Selecting States in CSS
Consumers of the component, as well as the component's internal
Shadow DOM, can style elements based on these custom identifiers using
:state().
External Styling: Consumers style the element from the document level:
toggle-switch:state(checked) {
border-color: #007bff;
background-color: #e6f0ff;
}Internal Styling (Shadow DOM): The component can
style itself internally using the :host() functional
pseudo-class:
:host(:state(checked)) {
--thumb-position: 100%;
}Key Benefits
- Strong Encapsulation: States can only be set or
modified by the component’s internal JavaScript. External code cannot
modify the state via
setAttributeorclassList. - Standard-Aligned API: Custom components can mimic the styling behavior and ergonomics of native platform controls.
- Cleaner HTML: The rendered HTML remains clean, with
no unnecessary
aria-*duplications or state classes cluttering the element markup.