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 APIinternals.stateselement.classList
Stored in HTML attributeNoYes
Designed for component stateYesGeneral 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, and selected.
  • 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

Play in Editor

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.

Selectors Level 4 - Custom State Pseudo-Class

HTML Living Standard - Custom State Pseudo-Class