Notes on CSS Toggles

Table of Contents

❌ This feature has been abandoned (at least for now). Our notes are likely out-of-date.

See the Unofficial Draft specification for details.

There are many use-cases that require “toggling” the state of one element by interacting with a different element. Many of these could be described in terms of show/hide states specifically:

The OpenUI project has been working on several new elements/attributes that could help with those use-cases at a high level – but we’re also interested in low-level tools to address more generic “toggle” behavior. Ideally a low-level ‘CSS Toggles’ feature can be used to power the higher-level HTML features that are in development – while also allowing for more style-centric toggles:

In all of the use-cases, we hope that accessibility can be built in by default – rather than tacked on (inconsistently) by authors.

Toggle States


The following syntax is slightly outdated. I’ll update it soon – but for now the ideas/approach are still roughly accurate. See the explainer for up-to-date developments.

The goal is that we can describe an element as having toggle states, which can be “shared by” or “forwarded to” other elements – similar (but not identical) to the way labels share state with their related input.

One of the difficult goals for making this CSS-centered, is to ensure we don’t require hard-coded links (especially unique-IDs) for making the connection between a toggle-trigger, and any toggle-targets.

In my mind, the ideal would be to give each toggle a custom-ident, which triggers can update, and targets can subscribe to.

Counter-like proposal

That’s the approach that Tab and I have taken in our initial syntax exploration. The key feature here is that named toggles work in the same way as CSS counters:

Non-exclusive toggles:

/* establish the toggle & interaction */
tab {
toggle: --card 2;

/* active styles */
tab:checked(--card) {}
content:checked(--card) {}

Exclusive toggle groups

Using the same HTML structure as above…

tabs {
toggle-group: --tabs hide show;

tab {
toggle-item: --tabs;

/* active styles */
tab:checked(--tabs) {}
content:checked(--tabs) {}

Default toggle-states

Default states will need to be established by the HTML, using a new attribute that accepts the toggle-name, and the state.

<article toggled="--colors dark">

By default:

This behavior (and naming?) should also match the provided pseudo-class:

article {} /* initial state */
article:toggled(--colors) {} /* first checked state */
article:toggled(--colors dark) {} /* explicit state */

Open Questions & Potential Issues

This work will be brought to CSSWG for initial feedback, with continued development in OpenUI before it becomes a specification.

New elements

We also hope to provide higher level controls, based around a new HTML element or elements. That work is also happening in the OpenUI CG.

Spicy Sections

Based on that research, and some discussion of “Design Affordance Control” – Brian Kardell built a web component spicy-section.

In a native implementation, we could imagine improving the syntax, but this gives us an opportunity to explore the approach. Specific names always need to go through a process of bike-shedding.

The element itself doesn’t do anything until you add an affordance to it. Most directly, you can do that with an HTML attribute:

<spicy-section affordance="tab-bar">

In HTML, an affordance-like attribute makes sense for changing which collapse/tab/normal affordances we see. But when we port that to CSS – an affordance property – it feels strange.

The current demo uses a property with inline media-queries:

spicy-section {
[screen and (max-width: 40em) ] collapse |
[screen and (min-width: 60em) ] tab-bar

We could break that out into:

@media screen and (max-width: 40em) {
spicy-section { affordance: collapse; }

@media screen and (min-width: 60em) {
spicy-section { affordance: tab-bar; }

That looks pretty good to me… Except that, as far as I know, we don’t have a lot of properties that only apply to one single element type. Maybe if we count content on generated elements?

That makes me wonder if we’ve designed a lower-level concept which could be applied to existing block sectioning elements – div, section, main, article, etc. Why not allow the new attribute & property on any of them?

This is still higher-level than the totally generic CSS Toggles described above. In addition to setting up toggles for you, it provides show/hide functionality, the ability to generate a tab bar, etc.

But it seems like that is mostly be handled through an attribute & property. The goal of a new element would be to set a specific affordance out-of-the-box, and we’re not really doing that. The element is only acting as a target for the new attribute & CSS property – which apply all the affordances.

So we could also imagine that any new element would instead provide a specific preset affordance? Something like a <tabs> or <accordion> element, that defaults to a specific affordance? Is that useful? Do people ever want the same affordance at all screen sizes?

Spicy questions

Let’s assume all the toggle/a11y logic & behavior can be handled in CSS Toggles.

Meeting notes

Prior art for CSS impacting tab order

Need to manage a11y roles

.tab { toggle-states: --foo; }
.card { content-visibility: toggle(--foo); }
.card:checked(--foo) { /* additional CSS */ }

With that, CSS alone can infer:

Maintaining state:

Path forward: