Skip to content

wvanim/css_state

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

Subject: [CSS] Proposal: state property and :state() pseudo-class – declarative state-based child views

To: www-style@w3.org


Introduction

Many common UI patterns involve showing one child element among several possible views depending on a current state. Examples include:

  • tab panels
  • carousels
  • button visual states (normal, hover, active, disabled)
  • multi-step forms
  • loading/success/error panels
  • paginated content sections
  • modal dialogs with different modes

Today, implementing these patterns typically requires JavaScript class toggling, DOM restructuring, index-based selectors (:nth-child), or radio/checkbox hacks using :checked.

CSS currently lacks a declarative mechanism to map a named state to a child view within a container. This proposal introduces a minimal primitive that stays within the fundamental CSS model: selectors → styles.


Proposal

1. A named state on the container

The container exposes a state value via a new state property:

.container {
  state: login;
}

Or driven by a CSS custom property:

.container {
  state: var(--view);
}

2. Children declare their state name

Children declare which state they represent via a new state-name property:

.login-view   { state-name: login; }
.error-view   { state-name: error; }
.loading-view { state-name: loading; }

3. The browser exposes the state in selectors

A new pseudo-class :state() allows selecting a container based on its current state:

.container > [state-name]               { display: none; }
.container:state(login) > .login-view   { display: block; }
.container:state(error) > .error-view   { display: block; }

4. Transitions work naturally

Since everything is selector-based, CSS transitions work without any special logic.

Simple fade using opacity and visibility:

.login-view,
.error-view {
  opacity: 0;
  visibility: hidden;
  transition: opacity 3s, visibility 3s;
}

.container:state(login) > .login-view {
  opacity: 1;
  visibility: visible;
}

.container:state(error) > .error-view {
  opacity: 1;
  visibility: visible;
}

Note: display: none is not animatable. Using opacity + visibility instead allows smooth transitions while keeping the element out of interaction when hidden.

Fade from display: none using @starting-style:

CSS now provides @starting-style (CSS Transitions Level 2) to animate elements appearing from display: none:

.login-view {
  display: none;
  opacity: 1;
  transition: opacity 3s;
}

.container:state(login) > .login-view {
  display: block;
}

@starting-style {
  .container:state(login) > .login-view {
    opacity: 0;
  }
}

This allows a full 3-second fade-in when a child becomes visible, even from display: none, without any JavaScript.

5. Driven by CSS interactions

Via :has() — zero JavaScript:

.button {
  state: normal;
}
.button:has(:hover)  { state: hover; }
.button:has(:active) { state: active; }

.button > [state-name]           { display: none; }
.button:state(normal) > .normal  { display: block; }
.button:state(hover)  > .hover   { display: block; }
.button:state(active) > .active  { display: block; }

6. Driven by minimal JavaScript

Via custom property:

element.style.setProperty("--view", "error");

Or directly:

element.state = "error";

Behavior

  • state is a regular CSS property set on the container
  • state-name is a CSS property set on direct children
  • :state() matches a container whose current state equals the given name
  • The cascade works normally — no implicit side effects on other elements
  • No new event system is introduced
  • Existing CSS features (:has(), custom properties, @starting-style, pseudo-classes) drive state changes and transitions naturally

Formal definition

The :state(<ident>) pseudo-class matches elements whose internal state attribute equals the given identifier.

The state property updates this internal state attribute based on its computed value. The state property applies to the container element.

State switching is intended for direct child views of that container.


Why this approach?

  • Respects CSS architecture : selectors → styles, no property acting implicitly on other elements
  • Declarative : no JavaScript needed for purely visual state switching
  • Semantic : state names are meaningful, unlike index-based selectors
  • Transitions : work naturally via opacity + visibility or @starting-style, no special logic required
  • Flexible : driven by CSS interactions, custom properties, or minimal JavaScript
  • Consistent : :state() is coherent with :has(), :is(), :where()

Prior art and limitations of current solutions

  • JavaScript class toggling : couples UI logic to JavaScript for a purely visual concern
  • :nth-child : fragile, non-semantic, does not accept custom properties
  • Radio/checkbox hacks : misuse of form elements, limited to binary states
  • :has() + sibling combinators : unwieldy for more than a few states
  • Container style queries : verbose, requires a separate rule per state and per child

Open questions for discussion

  1. state property naming — does state conflict with existing properties or future proposals? Alternatives : ui-state, view-state, active-state.

  2. state-name as HTML attribute or CSS property — should state-name be an HTML attribute (state="login") or strictly a CSS property? Both could coexist.

  3. Scope of :state() — should :state() match only on direct children, or on any descendant?

  4. Children without state-name — should they always be visible, always hidden, or controlled by an explicit keyword (e.g., state-name: *)?

  5. Initial value of state — should the default be an empty string (no children displayed), a keyword none, or the first child's state name?

  6. Accessibility — should hidden children (not matched) be automatically removed from the accessibility tree, as with display: none?

  7. Relation to display: state — an alternative formulation using display: state was considered. It offers more concision but introduces a property acting implicitly on children, which departs from the CSS model. Should both approaches be explored in parallel?

  8. Relation to Custom Element states — Web Components already expose a :state() pseudo-class. Should this proposal reuse the same mechanism or define a separate one?


Example gallery

Tab panels

.tabs { state: var(--tab); }
.tabs > [state-name]                    { display: none; }
.tabs:state(home)     > .home-panel     { display: block; }
.tabs:state(settings) > .settings-panel { display: block; }
<div class="tabs" style="--tab: home">
  <div class="home-panel"     state-name="home">Home content</div>
  <div class="settings-panel" state-name="settings">Settings content</div>
</div>

Button states

button { state: normal; }
button:has(:hover)  { state: hover; }
button:has(:active) { state: active; }

button > [state-name]          { display: none; }
button:state(normal) > .normal { display: block; }
button:state(hover)  > .hover  { display: block; }
button:state(active) > .active { display: block; }
<button>
  <span class="normal" state-name="normal">Click me</span>
  <span class="hover"  state-name="hover" >Click me (hover)</span>
  <span class="active" state-name="active">Click me (active)</span>
</button>

Server response states with fade

.form { state: var(--form-state); }

.form > .idle,
.form > .loading,
.form > .valid,
.form > .error {
  opacity: 0;
  visibility: hidden;
  transition: opacity 3s, visibility 3s;
}

.form:state(idle)    > .idle    { opacity: 1; visibility: visible; }
.form:state(loading) > .loading { opacity: 1; visibility: visible; }
.form:state(valid)   > .valid   { opacity: 1; visibility: visible; }
.form:state(error)   > .error   { opacity: 1; visibility: visible; }
element.style.setProperty("--form-state", "error");

Conclusion

This proposal introduces two minimal primitives — a state property and a :state() pseudo-class — that enable declarative state-based child views while fully respecting the CSS architecture of selectors → styles.

It covers a very common UI pattern today handled by JavaScript, and integrates naturally with existing CSS features such as :has(), custom properties, @starting-style, and transitions.

I would welcome feedback from the working group and the community on the open questions and the overall direction.

Thank you for your time and consideration.


About

CSS proposal addressing a very common UI pattern: showing one child view among several, depending on a current state (tabs, carousels, button states, forms, etc.). The proposal introduces two minimal primitives: a state property on the container a :state() pseudo-class to drive selectors It stays within the fundamental CSS model.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors