CSS :state() Pseudo-Class
The CSS :state() pseudo-class selects custom elements based on custom states defined through JavaScript. It works with the ElementInternals API and CustomStateSet to let reusable web components expose internal states to CSS without adding or removing classes or attributes on the custom element.
:state() Syntax
:state(custom-state-name) {
property: value;
}
The :state() pseudo-class accepts a single custom state name as its argument. It matches an element when that name is present in the element's custom state set.
Basic Example
status-card:state(active) {
border-color: #228b22;
}
Selects a <status-card> custom element when its active custom state is set.
Another State
status-card:state(loading) {
opacity: 0.6;
}
Styles the custom element when it has the loading state.
Multiple State Conditions
status-card:state(active):state(selected) {
outline: 2px solid #006699;
}
Matches when both custom states are present.
Parentheses Required
Unlike :hover or :checked, the :state() pseudo-class requires parentheses containing a custom state name.
Not a String Argument
Use :state(active), not :state("active"). The argument is a CSS identifier rather than a quoted string.
How :state() Works
Custom elements can maintain internal states using JavaScript. The :state() pseudo-class allows CSS to respond to those states.
Step 1 - Create a Custom Element
class StatusCard extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
}
customElements.define("status-card", StatusCard);
The custom element obtains an ElementInternals object.
Step 2 - Add a Custom State
this._internals.states.add("active");
Inside the custom element's class, this adds the active state to its custom state set.
Step 3 - Style the State
status-card:state(active) {
background-color: #e6f4ea;
}
The CSS rule matches when the custom element has the active state.
Step 4 - Remove the State
this._internals.states.delete("active");
Removing the state causes the selector to stop matching.
Automatic Styling
The browser updates selector matching when the custom state set changes. No additional class or attribute is required.
Custom Elements
Custom elements are reusable HTML elements defined by JavaScript. They allow developers to create components with their own behavior, styling, and state management.
Custom Element Markup
<status-card>Ready</status-card>
The element name contains a hyphen, as required for ordinary autonomous custom element names.
Define the Element
class StatusCard extends HTMLElement {
constructor() {
super();
}
}
customElements.define("status-card", StatusCard);
The customElements.define() method registers the custom element.
Add Internal State Support
class StatusCard extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
}
customElements.define("status-card", StatusCard);
The attachInternals() method provides access to the element's internal features, including custom states.
Custom Elements and CSS
Custom elements can be styled with ordinary selectors, attributes, classes, and supported pseudo-classes. The :state() pseudo-class adds a way to style them using their internal custom states.
ElementInternals API
The ElementInternals API provides internal capabilities for custom elements. Its states property exposes the element's CustomStateSet.
Attach Internals
this._internals = this.attachInternals();
Called inside a custom element, this creates or retrieves its associated internals object. A custom element can attach internals only once.
Access Custom States
const states = this._internals.states;
The states property provides the custom state set.
Add a State
this._internals.states.add("expanded");
Check a State
const expanded = this._internals.states.has("expanded");
Remove a State
this._internals.states.delete("expanded");
Internal API Access
The internals object is normally retained by the custom element's implementation. External code should use the component's documented public methods or properties rather than depending on its internal fields.
CustomStateSet
A CustomStateSet stores the custom state names associated with a custom element. Its interface provides methods for adding, removing, checking, and clearing states.
Common Methods
| Method | Description |
|---|---|
add("active") | Adds a custom state. |
delete("active") | Removes a custom state. |
has("active") | Checks whether a custom state exists. |
clear() | Removes all custom states. |
forEach() | Runs a callback for each custom state. |
Add Multiple States
this._internals.states.add("active");
this._internals.states.add("selected");
Check a State
if (this._internals.states.has("active")) {
console.log("The component is active");
}
Remove a State
this._internals.states.delete("active");
Clear All States
this._internals.states.clear();
Set Behavior
Adding a state that already exists does not create a duplicate entry.
Adding and Removing States
Custom states can be updated in response to user interactions, component methods, or application data.
Add an Active State
this._internals.states.add("active");
Remove an Active State
this._internals.states.delete("active");
Toggle a State
const states = this._internals.states;
if (states.has("active")) {
states.delete("active");
} else {
states.add("active");
}
CSS Response
status-card {
border: 2px solid #ccc;
}
status-card:state(active) {
border-color: #228b22;
}
The border color changes automatically as the custom state is added or removed.
No Class Changes Required
The element's class attribute does not need to change when its custom state changes.
Custom State Names
The argument passed to :state() identifies a custom state. Use valid CSS identifiers that are also accepted by CustomStateSet.
Valid Names
status-card:state(active) {
color: green;
}
status-card:state(is-loading) {
opacity: 0.6;
}
status-card:state(has-error) {
border-color: red;
}
Matching JavaScript Names
this._internals.states.add("is-loading");
The CSS argument must identify the same state name added by JavaScript.
Case Sensitivity
Custom state names are case-sensitive. For example, active and Active are different names.
No Leading Period
Use :state(active), not :state(.active). The state name is not a class selector.
No Quotation Marks
Use an identifier such as active rather than a quoted string.
Descriptive Naming
Choose names that describe the component's state, such as expanded, loading, selected, or has-error.
Multiple Custom States
A custom element can have more than one custom state at the same time.
JavaScript Example
this._internals.states.add("active");
this._internals.states.add("selected");
Style Each State
status-card:state(active) {
border-color: #228b22;
}
status-card:state(selected) {
background-color: #eaf6fc;
}
Require Both States
status-card:state(active):state(selected) {
outline: 2px solid #006699;
}
This selector matches only when both states are present.
Match Either State
status-card:is(:state(active), :state(selected)) {
font-weight: bold;
}
Matches when at least one of the two states is present.
Independent States
Custom states do not automatically exclude one another. If a component requires mutually exclusive states, its JavaScript implementation must enforce that behavior.
Custom States vs. CSS Classes
Both custom states and CSS classes can affect styling, but they are stored and managed differently.
Comparison
| Feature | Custom State | CSS Class |
|---|---|---|
| CSS selector | :state(active) | .active |
| JavaScript API | internals.states | element.classList |
| Stored in HTML attribute | No | Yes |
| Designed for component state | Yes | General purpose |
Class-Based Styling
status-card.active {
border-color: green;
}
Custom State Styling
status-card:state(active) {
border-color: green;
}
JavaScript Difference
this.classList.add("active");
this._internals.states.add("active");
The first statement changes the element's class attribute. The second updates its internal custom state set.
When to Use Each
Use classes for general styling and externally managed states. Use custom states when a custom element needs to expose its internal state to CSS.
Custom States vs. Attributes
HTML attributes can represent component configuration or state, while custom states provide a separate internal styling mechanism.
Attribute Selector
status-card[active] {
border-color: green;
}
Custom State Selector
status-card:state(active) {
border-color: green;
}
HTML Attribute
<status-card active>Ready</status-card>
The attribute is visible in the element's HTML markup.
Custom State
this._internals.states.add("active");
The custom state is stored in the component's custom state set rather than as an HTML attribute.
Attributes and States Are Independent
Adding an active attribute does not automatically add an active custom state. A component must explicitly synchronize them if that behavior is required.
Choosing an Approach
Use attributes when a state or configuration should be represented in markup. Use custom states when the component needs to expose internal state to CSS without reflecting it to an attribute.
Shadow DOM
Custom elements often use Shadow DOM to encapsulate their internal markup and styles. Custom states can be used to style the custom element itself and, through shadow-tree selectors, respond to its state.
Create a Shadow Root
const shadow = this.attachShadow({ mode: "open" });
External Styling
status-card:state(active) {
border-color: #228b22;
}
Styles the custom element from the document stylesheet when its custom state is active.
Internal Styling
:host(:state(active)) {
background-color: #e6f4ea;
}
Inside a shadow stylesheet, this selector styles the host when its custom state is active.
Style Internal Elements
:host(:state(active)) .message {
color: #228b22;
}
Styles an internal element with the class message when the host has the active state.
Encapsulation
Custom states provide a styling interface without requiring the component to expose its internal DOM structure.
Using :host() with :state()
The :host() pseudo-class function can combine with :state() in a shadow stylesheet to style a custom element based on its state.
Basic Example
:host(:state(active)) {
border: 2px solid #228b22;
}
Loading State
:host(:state(loading)) {
opacity: 0.6;
}
Expanded State
:host(:state(expanded)) .details {
display: block;
}
Collapsed State
:host(:not(:state(expanded))) .details {
display: none;
}
The two selectors can control the appearance of internal details based on whether the host has the expanded state.
Important Context
The :host() selector is intended for shadow-tree stylesheets. In ordinary document stylesheets, select the custom element directly with :state().
Custom States and ::part()
Custom states can also be combined with the ::part() pseudo-element to style exposed parts of a custom element.
HTML Inside a Shadow Root
<div part="panel">
Component content
</div>
External CSS
status-card:state(active)::part(panel) {
background-color: #e6f4ea;
}
Styles the exposed panel part when the custom element has the active state.
Another State
status-card:state(loading)::part(panel) {
opacity: 0.6;
}
Part Exposure Required
The internal element must expose a part name through the part attribute for external ::part() styling to work.
Component Styling Interface
Combining custom states with exposed parts allows component authors to offer controlled styling options without exposing all internal markup.
:state() Specificity
The :state() pseudo-class contributes a specificity of 0-1-0, like other standard pseudo-classes.
Specificity Examples
| Selector | Specificity |
|---|---|
:state(active) | 0-1-0 |
status-card:state(active) | 0-1-1 |
.card:state(active) | 0-2-0 |
:state(active):state(selected) | 0-2-0 |
#card:state(active) | 1-1-0 |
:where(:state(active)) | 0-0-0 |
Specificity Example
status-card {
border-color: #ccc;
}
status-card:state(active) {
border-color: #228b22;
}
When the custom element has the active state, the more specific rule applies, assuming other cascade factors are equal.
Multiple States
Each explicit :state() pseudo-class contributes its own specificity.
Low-Specificity Alternative
:where(status-card:state(active)) {
border-color: #228b22;
}
The :where() pseudo-class reduces the specificity of the complete selector to zero.
Practical Uses
The :state() pseudo-class is useful for styling reusable custom elements according to their internal state.
Expanded Component
custom-panel:state(expanded) {
border-color: #006699;
}
Loading Indicator
data-widget:state(loading) {
opacity: 0.6;
}
Selected Item
custom-option:state(selected) {
background-color: #eaf6fc;
}
Validation State
custom-field:state(has-error) {
border-color: #b3261e;
}
Active Component
status-card:state(active) {
background-color: #e6f4ea;
}
Multiple Conditions
data-widget:state(loading):state(expanded) {
outline: 2px solid #006699;
}
These examples require custom elements whose JavaScript implementations define and maintain the corresponding states.
Limitations
The :state() pseudo-class is designed for custom elements and depends on the Custom State API.
Requires a Custom State
The selector only matches when the requested state is present in the element's custom state set.
Not a General HTML Attribute Selector
Adding an HTML attribute named active does not automatically make an element match :state(active).
JavaScript Required to Define States
Custom states are created and maintained through the custom element's JavaScript implementation.
Not Automatically Linked to Classes
Adding the class active does not add the custom state active.
Custom Element Lifecycle
The custom element must be registered and initialized before its custom state behavior becomes available.
No Automatic Accessibility Semantics
A custom state does not automatically provide ARIA attributes, keyboard interaction, or accessible component behavior.
State Names Must Match
The identifier used in CSS must match the name maintained in the custom state set.
Browser Compatibility
Support depends on the browser's implementation of :state(), ElementInternals.states, and related APIs.
Fallback Planning
For applications supporting older browsers, consider class-based or attribute-based styling as a fallback.
Best Practices
- Use
:state()to expose meaningful internal states of custom elements to CSS. - Choose descriptive custom state names such as
expanded,loading, andselected. - Keep state management inside the custom element's implementation.
- Use public methods or properties to let other scripts change component behavior.
- Do not use custom states as replacements for accessibility semantics or keyboard support.
- Use attributes when component configuration should be visible and editable in HTML.
- Use classes for general-purpose styling and externally managed presentation states.
- Use
:host(:state(...))when styling a stateful host from inside its shadow stylesheet. - Test multiple simultaneous states when a component supports them.
- Check browser compatibility for the Custom State API before relying on it in production.
:state() Example
This example creates a custom element with an active state. Click Toggle State to add or remove the state using ElementInternals.states. The CSS :state(active) selector changes the component's appearance automatically.
HTML
<status-card id="demo-card">Component is inactive</status-card>
<button type="button" id="toggle-state">Toggle State</button>
<p id="state-status" role="status" aria-live="polite">State: inactive</p>
CSS
status-card {
display: block;
padding: 16px;
border: 2px solid #999;
border-radius: 6px;
background-color: #f2f2f2;
}
status-card:state(active) {
border-color: #228b22;
background-color: #e6f4ea;
color: #176b31;
}
#toggle-state {
margin-top: 12px;
padding: 8px 12px;
cursor: pointer;
}
JavaScript
class StatusCard extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
toggleActive() {
const states = this._internals.states;
const active = states.has("active");
if (active) {
states.delete("active");
} else {
states.add("active");
}
this.textContent = active ? "Component is inactive" : "Component is active";
return !active;
}
}
customElements.define("status-card", StatusCard);
const card = document.getElementById("demo-card");
const status = document.getElementById("state-status");
document.getElementById("toggle-state").addEventListener("click", () => {
const active = card.toggleActive();
status.textContent = active ? "State: active" : "State: inactive";
});
Browser Result
Note: The custom element starts in the inactive state. Clicking Toggle State adds or removes the active custom state. The CSS selector updates the appearance without adding a class or HTML attribute to the component.
Browser Support
Baseline: Newly available. The :state() pseudo-class and the related Custom State API are supported in current versions of major browsers, but older versions may not support them. Check the compatibility of both the selector and ElementInternals.states when targeting older browsers.
For current compatibility information, visit Can I Use? and search for CSS :state() or custom states.
Specifications
The :state() pseudo-class is defined in CSS Selectors Level 4. The CustomStateSet and ElementInternals.states APIs are defined in the HTML Living Standard.
