CSS Toggle States

This is heavily based on a previous proposal by Tab; the active work happening in OpenUI around popups, tabs, and accordions; and a work-in-progress proposal by Nicole Sullivan & Robert Flack.

The key feature here is that named toggles work in the same way as CSS counters:

Non-exclusive Accordion (with or without cards)

<accordion>
 <!-- Express initial states for a given toggle in html,
      optionally with one of multiple states (e.g. `--card 3`) 
      -->
 <card toggled="--card">
  <tab>...</tab>
  <content>...</content>
 </card>
 <card>
  <tab>...</tab>
  <content>...</content>
 </card>
 <card>
  <tab>...</tab>
  <content>...</content>
 </card>
</accordion>
tab {
  toggle: --card 2;
  /* shorthand for:
   * toggle-states: --card 2;
   * toggle-set: --card;
   */
}

content:checked(--card) {
 /* Toggles use counter scoping rules
  * So this sees the toggle (and value) established
  * by its previous sibling <tab>
  */
  …
}

Accordion again, but with unpredictable tab/content order

<accordion>
 <card>
  <content>...</content>
  <tab>...</tab>
 </card>
 <card>
  <tab>...</tab>
  <content>...</content>
 </card>
 <card>
  <tab>...</tab>
  <content>...</content>
 </card>
</accordion>
card {
 toggle-states: --card 2;
}

tab {
  /* toggle established by parent,
   * so <tab> just opts into *manipulating*
   * the toggle
   */
  toggle-set: --card;
}

content:checked(--card) {
  /* This time, the toggle comes from the parent
}

Details element

<details2>
 <summary>...</>
 <content>...</>
</details2>
<style>
details2 {
 toggle-states: --show 2;
}
summary {
 toggle-set: --show;
}
content {
 display: none;
}
content:checked(--show) {
 display: block;
}

Self-contained Checkbox w/ Named States

<check-box></>
<style>
check-box {
 toggle-self: 3 off on unknown;
 /* toggle-self limits the toggle's scope
  * to just the element itself.
  * We *think* this is just a safety improvement,
  * not actually a needed new ability.
  */
}
check-box:checked(self off) {...}

Tabs / Exclusive Accordion (with or without cards)

<tabs>
 <card>
  <tab>...</tab>
  <content>...</content>
 </card>
 <card>
  <tab>...</tab>
  <content>...</content>
 </card>
 <card>
  <tab>...</tab>
  <content>...</content>
 </card>
</tabs>
tabs {
 toggle-group: --show 2;
 /* -group establishes a scope for sub-counters,
  * of which only one can be active at a time
  * Same grammar as -states, so all sub-counters are identical.
  */
}
tab {
 toggle-item: --show;
 /* creates a sub-counter tied to the --show group */
 /* because --show is a group, only one can be active */
}
content:checked(--show) {
 /* sees the --show counter from its sibling */
}

Arbitrary toggler element & target positions

<html toggle-scope="colors">
<button toggle-btn="colors"></>
<section toggle-target="colors"></>
<style>
[toggle-scope] {
 toggle-states: attr(toggle-scope) [...];
 /* toggles can be named by a string
  * instead of a custom ident
  */
}

button {
 toggle-set: attr(toggle-btn);
}

[toggle-target]:checked(attr(toggle-target)) {
  /* Requires some magic here,
   * but attr information *is* known
   * during selector evaluation.
   * Hopefully okay?
   */
}

Tabs up front ❌

The code here represents some of our early attempts, but we do not consider this use-case solved or part of the current proposal.

<tabs>
  <tab>...</tab>
  <tab>...</tab>
  <tab>...</tab>
  <content>...</content>
  <content>...</content>
  <content>...</content>
</tabs>
<style>
/* tab-bar mode */
tabs {
 display: grid;
 grid-template-rows: auto 1fr;
 counter-reset: tabs contents;
 toggle-states: --tab group 2;
}
tabs::grid-cell(1 / 1) {
 area-name: foo;
 display: flex;
}
tab {
 flow-into-grid-area: foo;
 counter-increment: tabs;
 toggle-set: --tab counter(tabs);
}
content {
 grid-row: 2;
 counter-increment: contents;
 toggle-read: --tab counter(contents);
}

.radiogroup {
 toggle-states: --radio group 2;
}
.radio {
 toggle-set: --radio;
 toggle-read: --radio;
}

Is this a potential solution?

tabs {
 toggle-group: --show 2;
 counter-reset: tabs contents;
}
tab {
 toggle-item: --show counter(tabs);
 /* toggle-item could generically allow named items,
  * and accept counter() as one way to assign those names 
  */
 counter-increment: tabs;
}
content {
  counter-increment: contents;
}
content:checked(--show counter(contents)) {
 /* sees the --show counter from its sibling */
}

Open Questions & Potential Issues

  • All syntax needs bikeshedding…
    • clarity around toggle-names, state-counts, and state-names
    • is :checked() the right syntax for a pseudo?
    • what are the default states, and can we name them (e.g. on/off)?
  • Can a11y be built-in and handled automagically?
    • different toggle types (show/hide, files, etc) may need different a11y handling,
    • ways to opt-into those semantics?
    • it should not be easy to create in-accessible interfaces
  • How do we handle content-visibility?
    • Especially for linking into hidden tabs, etc?
  • How does it interact with animations & transitions?
  • Can state be maintained across navigation, like form controls?