Notes on CSS Toggles

Table of Contents

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 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: