CSS Toggles (in JS)

What are CSS Toggles?

The goal of this (work in progress) feature is to make it possible for CSS to manage presentational state for patterns such as tabs/accordians, carousels, color modes, etc. There are still many questions to be answered around the scope, syntax, and (most importantly) accessibility of a feature like this in CSS.

This polyfill is designed to help us explore those questions. It implements the draft spec syntax as currently written, where possible -- in addition to some of the extensions proposed in our explainer.

The current polyfill implementation is a naive attempt to uncover and document any issues in the spec — which means that several of the following demos are currently inaccessible. While we hope that some of those issues can be resolved here in the polyfill, others may require changes to the spec itself, or access to browser-internals that cannot be polyfilled well using JS.

We're excited for you to play with this, suggest additional use-cases, help uncover undocumented issues, and provide us with feedback:

Global color toggle

html {
  toggle-root: mode [auto light dark];

html:toggle(mode light) { ... }
html:toggle(mode dark) { ... }

.mode-btn {
  toggle-trigger: mode;

Binary self-toggle switches

Issue 20: There's a conflict between the HTML listitem role and the added ARIA role of 'button' used to make these interactive.

.todo li {
  toggle: todo self;
  list-style-type: '❌ ';

.todo li:toggle(todo) {
  list-style-type: '✅ ';

Accordion/disclosure components

The goal of toggle-visibility is for browsers to handle accessibility and discoverability by default. We'll keep working to improve the polyfill a11y as well, but may not be able to achieve the same results.

Issue 13: While aria-expanded may work in some situations, it has the same issues listed above with conflicting semantics/roles.

Establish a toggle
.accordion>dt {
  toggle: glossary;
Toggle item visibility
.accordion>dd {
  toggle-visibility: toggle glossary;
Style the summary
.accordion>dt::marker {
  content: '👉🏽 ';

.accordion>dt:toggle(glossary) {
  background-color: var(--brand);
  color: var(--bg);

.accordion>dt:toggle(glossary)::marker {
  content: '👇🏽 ';

Tree view

Issue 23: Does 'tree view' require different semantics than simply 'nested disclosures' (which may better describe the current behavior)?

.nested {
  toggle: tree;

.nested+ul {
  toggle-visibility: toggle tree;

Tabs or exclusive accordions

Establishing Tabs
panel-tab {
  toggle: tab 1 at 0 group sticky;
  grid-row: tab;
Styling Content
panel-tab:toggle(tab) {
  background-color: var(--callout);

panel-card {
  toggle-visibility: toggle tab;
  grid-area: card;

Named states

Issue 21: These act like radio buttons, but the proper a11y handling is not obvious from the syntax.

.colors {
  toggle-root: colors [grape green blue red] at blue;

.colors button {
  toggle-trigger: colors;

/* for each color */
.colors button.grape {
  toggle-trigger: colors grape;

.show-colors:toggle(colors grape) {
  background-color: var(--grape-9);

State machine transitions

This functionality & syntax is proposed in the explainer as syntax sugar on top of the existing functionality. However, it's not yet clear that there are entirely presentational (CSS-only) use-cases. If you have ideas, we'd love to hear from you. The following example would involve JS in production.

Issue 22: It is not clear in the a11y tree which 'transitions' are currently allowed/disabled, and the resulting generated content is not propery announced or selectable.

@machine request {
  idle {
    try: loading;

  loading {
    resolve: success;
    reject: failure;
    reset: idle;

  failure {
    try: loading;
    reset: idle;

  success {
    reset: idle;

/* establish a toggle based on the machine */
.request {
  toggle-root: machine(request);

/* the individual trigger buttons call 'transitions' */
.request button[data-do="try"] {
  toggle-trigger: request do(try);