CSS Toggles

Unofficial Proposal Draft,

More details about this document
This version:
http://tabatkins.github.io/css-toggle/
Issue Tracking:
CSSWG Issues Repository
Inline In Spec
Editors:
Tab Atkins Jr. (Google)
Miriam E. Suzanne (Invited Expert)
Suggest an Edit for this Spec:
GitHub Editor

Abstract

This specification defines a way to associate a "toggleable value" with an element which can be used in Selectors to select an element, and declarative ways to set and modify this value on the element.

CSS is a language for describing the rendering of structured documents (such as HTML and XML) on screen, on paper, etc.

Status of this document

This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C technical reports index at https://www.w3.org/TR/.

Please send feedback by filing issues in GitHub (preferred), including the spec code “css-toggle” in the title, like this: “[css-toggle] …summary of comment…”. All issues and comments are archived. Alternately, feedback can be sent to the (archived) public mailing list www-style@w3.org.

This document is governed by the 2 November 2021 W3C Process Document.

This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent which the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy.

1. Introduction

This section is not normative.

Many user-interface languages define elements which can have "toggleable state", which can be modified by user interaction and responded to by the element. For example, in HTML, the <input type=checkbox> has a "checked" state which toggles between true and false when the user activates the element, and which is selected by the :checked pseudoclass.

These sorts of elements are extremely useful, to the point that authors sometimes abuse them to get the same functionality on other elements:

The following markup example shows how to lightly abuse HTML semantics to declaratively use toggleable state:
<ul class='ingredients'>
  <li><label><input type=checkbox><span>1 banana</span></label>
  <li><label><input type=checkbox><span>1 cup blueberries</span></label>
  ...
</ul>
<style>
input[type='checkbox'] {
  display: none;
}
input[type='checkbox']:checked + span {
  color: silver;
  text-decoration: line-through;
}
</style>

In this markup, one can cross out ingredients as they’re used in the recipe by simply clicking on them, without any scripting being involved. The label "transfers" the activation to the (hidden) checkboxes, which are then used to style the span.

This module generalizes this ability and allows it to be applied to any element via CSS, so authors do not have to abuse host language semantics for styling purposes. It defines a a way of attaching lightweight "state" to an element via CSS (toggle-root), defining how user interactions can change that state (toggle-trigger), and responding to that state from CSS (:toggle() pseudo-class). Scripting can also create, modify, and respond to this state, to accommodate more complicated scenarios.

It also defines how to infer reasonable accessibility semantics from the toggle structure, making it simpler and more reliable to produce accessible pages using these sorts of basic interactivity without the author having to manually annotate a page with ARIA attributes or similar.

1.1. Terminology

Any element can become a toggle root, meaning it hosts one or more toggles. Each toggle has a name, a value between 0 and some maximum, and a few other bits of metadata, all of which can be set via the toggle-root property. The toggle is visible to the toggle root, its descendants, and possibly its siblings and their descendants (if the toggle says they can); any element that can "see" a toggle can use the :toggle() pseudo-class to select the element based on the toggle’s value.

Any element that can see a toggle can also trigger the toggle, changing its value when the element is "activated", via the toggle-trigger property. This means you can have elements that self-trigger their own toggle, like checkboxes, but also have toggles that are visible to wide sections of a page and are triggered by buttons (or tabs, or headings, etc) inside that section, so multiple elements can use :toggle() to respond to the toggle.

Toggles can also be grouped together, so that only one of the toggles in the group can have a non-zero value at a time, like how radio buttons work in HTML.

2. Toggle Concepts

A toggle is a struct associated with an element, which represents something that can be toggled on or off by user action, and matched with Selectors. Toggles have the following items:

name

A <custom-ident>.

value

A <custom-ident> or non-negative <integer>: either 0 (the inactive value) or a value 1 or higher (the active values).

states

Either an integer greater than or equal to 1, indicating the nominally maximum integer value, or a list of unique state names, each of which are <custom-ident>s.

This also defines the maximum state of a toggle, as either the states value (if it’s an integer) or the length of states minus 1 (if it’s a list).

Note: Toggles can be set to integer values larger than than the "maximum", or to ident values not given in the list of names, but when a toggle is incremented or decremented it will treat all such values as out-of-bounds and bring the value back within the range defined by states.

group

A boolean indicating whether the toggle is part of a toggle group (of the same name) or not.

scope

An enum indicating what sort of scope the toggle uses. It can have two values:

  • "wide", indicating the toggle has wide scope (it’s visible to the element, its descendants, and its following siblings and their descendants).

  • "narrow", indicating the toggle has narrow scope (it’s visible to the element and its descendants only).

overflow

An enum (either "cycle", "cycle-on" or "sticky"), specifying how to react when a toggle activation would increment the value above its maximum or below its minimum.

The precise details are defined in change a toggle, but in short:
  • For cycle, incrementing past the maximum resets the value to 0, and decrementing below 0 resets it to the maximum.

  • For cycle-on, incrementing past the maximum resets the value to 1, and decrementing it below 1 resets it to the maximum.

    (In other words, the toggle "stays on" once it reaches an active value, rather than cycling back to an inactive value.)

  • For sticky, incrementing past the maximum value resets the value to the maximum, and decrementing it below 0 resets it to 0.

Toggles are persistent state on an element. Once created, none of their items are affected by CSS, and only their value can be changed by user interaction. (Scripting can change the other items, however; see § 6 Scripting API.) An element can have any number of toggles.

To match values, given toggle (a toggle), a test value test value (an integer or <custom-ident>), and optionally states (a states value):
  1. If states was not passed, let states be toggle’s own states value.

  2. Let toggle value be toggle’s value.

  3. If toggle value and test value are both integers, and the same integer, return true.

  4. If toggle value and test value are both <custom-ident>s, and are identical to each other, return true.

  5. If states is a list of <custom-ident>s, and toggle value is a <custom-ident> in the list, set toggle value to its index in the list.

    Do the same for test value if it is a <custom-ident>.

    If toggle value and test value are now both the same integer, return true.

  6. Otherwise, return false.


A toggle specifier is a struct associated with an element, with the same structure as a toggle. They serve multiple roles: they specify what toggles the element is expected to have on it (creating fresh ones if they don’t exist), provide default values for the newly-created toggle’s items, and override a toggle’s default behavior when it receives a toggle activation.

Toggle specifiers are defined by the toggle-root property; they are not persistent state on the element. An element can have any number of toggle specifiers.


A toggle group is a struct associated with an element, which groups together toggles of the same name so that only one can be in an active value at a time. Toggle groups have the following items:

name

A <custom-ident>.

scope

An enum indicating what sort of scope the toggle group uses. It can have two values:

  • "wide", indicating the toggle has wide scope (it’s visible to the element, its descendants, and its following siblings and their descendants).

  • "narrow", indicating the toggle has narrow scope (it’s visible to the element and its descendants only).

Toggle groups are defined by the toggle-group property; they are not persistent state on an element. An element can have any number of toggle groups.

A toggle is in a toggle group if its group boolean is true; if the element is in scope for a toggle group with the same name as the toggle, it’s in that group; otherwise, it’s in a document-wide implicit toggle group with the same name.


Toggles and toggle groups have a scope, defining what additional elements (beyond the element the toggle/toggle group is on) can see and interact with the toggle/toggle group.

If the toggle (toggle group) has wide scope, it’s visible to the element it’s defined on, its descendants its following siblings, and their descendants.

If the toggle (toggle group) has narrow scope, it’s visible to the element it’s defined on and its descendants.

Toggles of the same name "shadow" earlier ones; if multiple toggles of a given name would have overlapping scopes, an element is only in scope for the toggle created by the nearest preceding element in tree order. The same applies to toggle groups. However, toggles and toggle groups do not interfere with each other’s scopes; an element can "see past" a toggle to find a toggle group of the same name it’s in scope for, and vice versa.

2.1. Toggles and CSS Properties

To allow for toggles to be responded to by Selectors without causing any circularity issues, toggles themselves are invisible state on an element, separate from any CSS properties that might apply. The CSS properties merely define which elements can activate a toggle, and which elements can respond to a toggle activation (and how they do so).

For example, while an element needs toggle-root to establish a toggle, once a toggle is created, removing the toggle-root property does not affect the toggle.
.toggleable {
  toggle-root: foo 1;
  toggle-trigger: foo;
}
.toggleable:toggle(foo) {
  toggle-root: none;
  toggle-trigger: none;
}

In this example, the toggleable element declares that it can establish a foo toggle, and activate it. During a rendering update, the foo toggle is created on the element (initially with a value of 0, its inactive value); when the element is clicked, it’s incremented to its active value (1).

At this point, the :toggle(foo) rule begins to match, and removes the toggle-* properties. This does not remove the foo toggle or affect its value, however; it still exists and is still set to 1, so the :toggle() pseudo-class will continue matching. However, further activations of the element will no longer affect the foo toggle, since toggle-trigger was changed to none.

The foo toggle is thus "frozen" in the activated state (unless the author has other elements in the toggle’s scope that can also affect it, or tweaks the value manually via JS).

3. Creating a Toggle: the toggle-root property

Name: toggle-root
Value: none | <toggle-specifier>#
Initial: none
Applies to: all elements
Inherited: no
Percentages: n/a
Computed value: as specified
Canonical order: per grammar
Media: interactive
Animatable: no
<toggle-specifier> =
  <custom-ident>
  [
    <toggle-states> [at <toggle-value>]? ||
    <toggle-overflow> ||
    group ||
    self
  ]?
<toggle-states> = <integer [1,∞]> | '[' <custom-ident>{2,} ']'
<toggle-value> = <integer [0,∞]> | <custom-ident>
<toggle-overflow> = cycle | cycle-on | sticky

The toggle-root property causes toggles to be created on an element, and controls how the toggles are updated when they are activated.

none
The element has no toggle specifiers.

(This does not remove any toggles that have already been established on the element.)

<toggle-specifier>#
The element has one or more toggle specifiers, one per <toggle-specifier>, which determine how toggles will be initially created if they don’t already exist on the element, and how they react to being activated.

Each <toggle-specifier> is composed of several parts, most of which are optional, corresponding to the items of a toggle specifier. They specify how the newly-created toggle will be set up:

  • The initial <custom-ident> specifies the name. The <custom-ident> cannot be the keyword none.

  • The <toggle-states> specifies the states. If specified as an <integer>, it sets states to the given number; if specified as a bracketed list of <custom-ident>s, it sets states to a list containing those identifiers. If omitted, defaults to 1.

    If <toggle-states> is a bracketed list, and there are any repeated <custom-ident>s among its items, the property is invalid.

  • The <toggle-value>, if specified, specifies the initial value. If omitted, it’s set to 0.

  • The <toggle-overflow> keyword, if specified, specifies the overflow enum. If omitted, it’s set to "cycle".

  • The group keyword, if specified, sets the group boolean to true. If omitted, it’s set to false.

  • The self keyword, if specified, sets the scope enum to "narrow". If omitted, it’s set to "wide".

In addition to specifying how to create new toggles, a toggle specifier overrides the existing toggle’s states, overflow, and group, if they differ, allowing an existing toggle to have its behavior changed if necessary.

Revisiting the example in the Introduction, the same ingredient list can be specified in simple HTML and CSS:
<ul class='ingredients'>
   <li>1 banana
   <li>1 cup blueberries
  ...
</ul>
<style>
li {
  toggle: check self;
}
li:toggle(check) {
  color: silver;
  text-decoration: line-through;
}
</style>

The effect is identical to what was specified in the Introduction example, except the markup is much simpler and more semantic.

3.1. Toggle Creation Details

Each element has a list of established toggles, representing toggles on the element. These are created automatically by the toggle-root property, and/or manually by interacting with the Element.toggles map.

Define the precise point in update the rendering when toggles are created if toggle-root names a toggle that doesn’t exist on the element yet.

3.2. Linking Toggle Values: the toggle-group property

Name: toggle-group
Value: none | [ <custom-ident> self? ]#
Initial: none
Applies to: all elements
Inherited: no
Percentages: n/a
Computed value: as specified
Canonical order: per grammar
Media: interactive
Animatable: no

By default, each toggle’s value is independent; incrementing one has no effect an any other. The toggle-group property allows elements to link their toggles together: all toggles with the same name as the toggle group that are on elements in scope for the toggle group are linked, such that only one can be in an active value at a time, similar to HTML’s <input type=radio> element.

none
The element does not define a toggle group.
[<custom-ident> self?]#
The element defines one or more toggle groups, one per comma-separated item:
  • The <custom-ident> specifies the name, as the item’s value. The <custom-ident> cannot be the keyword none.

  • The self keyword, if specified, sets the scope enum to "narrow". If omitted, it’s set to "wide".

Only one toggle in a toggle group can be in an active value at a time; see toggle-trigger for details.

For example, toggle-group can be used to control a tabbed display, so that only one panel is displayed at a time:
<panel-set>
  <panel-tab>first tab</panel-tab>
  <panel-card>first panel</panel-card>
  <panel-tab>second tab</panel-tab>
  <panel-card>second card</panel-card>
  ...
</panel-set>
<style>
panel-set {
  /* The common ancestor sets up a group */
  toggle-group: tab;
}
panel-tab {
  /* Each tab creates a cycle-on toggle
    (so once it’s open, clicking again won’t close it),
    opts into the group,
    and declares itself a toggle activator */
  toggle: tab 1 group cycle-on;
}
panel-tab:first-of-type {
  /* The first tab also sets its initial value
    to be active */
  toggle: tab 1 at 1 group cycle-on;
}
panel-tab:toggle(tab) {
  /* styling for the active tab */
}
panel-card {
  /* cards are hidden by default */
  display: none;
}
panel-card:toggle(tab) {
  display: block;
}
</style>

Clicking on any tab will increment its corresponding toggle’s value from 0 to 1 (and the cycle-on keyword will keep it at 1 if activated multiple times), while resetting the rest of the tabs' values to 0. Each panel is in scope for the toggle defined by its preceding tab, so it can respond to the value as well.

Radio buttons have one particular tabbing/switching/activation behavior (they occupy a single tabindex spot; once reached you can move between them with arrow keys; moving to one auto-activates it), but not all groups will want that behavior. Accordions, in particular, probably don’t, and instead just want to effectively be independent checkboxes that happen to only have one active at a time.

We probably want to add a bool to toggle groups dictating this; are there more than these two behaviors to deal with?

3.3. Activating a Toggle: the toggle-trigger property

Name: toggle-trigger
Value: none | <toggle-trigger>#
Initial: none
Applies to: elements without existing activation behavior (see prose)
Inherited: no
Percentages: n/a
Computed value: the keyword none, or a list of <toggle-trigger> values
Canonical order: per grammar
Media: interactive
Animatable: no
<toggle-trigger> = <custom-ident> <trigger-action>?
<trigger-action> =
  [prev | next] <integer [1,∞]>? |
  set <toggle-value>

The toggle-trigger property specifies that an element can be activated to change the value of one or more toggles. It has the following values:

Consider adding support for an at-rule that defines machine states, the available events to trigger from each state, and the resulting state of each event. In that case <trigger-action> would need to also accept custom events, possibly marked by a new keyword or function.

none

The element does not manipulate any toggles.

<toggle-trigger>

If the <trigger-action> is omitted, it defaults to next 1. If the <trigger-action> specifies prev or next, but omits the following <integer>, it defaults to 1.

If the element already has existing activation behavior from the host language, this value does nothing.

Otherwise, the element becomes activatable, and when activated, for each comma-separated entry in the list, fires a toggle activation with the given <custom-ident> as a name, and the <trigger-action> action.

The <custom-ident> cannot be the keyword none.

A toggle activation is a struct with the following items:

name

The name of the toggle this is intended to activate.

action

A <trigger-action> value.

To fire a toggle activation on an element initial element with a toggle activation activation:
  1. Let el initially be initial element.

  2. If el has a toggle with the same name as activation, and initial element is in scope for the toggle, proceed to the next step. Otherwise, continue searching for a toggle:

    • If el has a previous sibling, set el to that element and return to step 2.

    • Otherwise, if el has a parent element, set el to that element and return to step 2.

    • Otherwise, return. (No toggle was found, so nothing happens.)

  3. Let t be the toggle on el with the same name as activation.

    Let old value be t’s value.

    Let tSpec be the toggle specifier on el with the same name as activation, if one exists.

    Change a toggle t, passing activation’s action, and an override spec of tSpec if it exists.

    Let new value be t’s value.

  4. If old value and new value are different, fire a toggle change event at el for the toggle t.

To change a toggle t, given an action action, and optionally a toggle specifier override spec:
  1. If override spec was passed, let states, group, and overflow be the correspondingly-named items from override spec.

    Otherwise, let states, group, and overflow be the correspondingly-named items from t.

  2. If action starts with the keyword set, then change t’s value to the value specified by action.

    If t does not match 0, given states, and group is true, then set the value of all other toggles in the same toggle group as t to 0.

    Return.

  3. If t’s value is an integer, let index be that integer. Otherwise, if states is a list of idents and t’s value is in the list, let index be the index of the first occurence of the value. Otherwise, let index be infinity.

  4. If states is an integer, let max index be that integer. Otherwise, let max index be one less than the length of states.

  5. If action starts with the keyword next, then increment index by action’s value. Then, based on the value of overflow:

    cycle

    If index is greater than maximum index, set index to 0.

    cycle-on

    If index is greater than maximum index, set index to 1.

    sticky

    If index is greater than maximum index, set index to maximum index.

Otherwise, if action starts with the keyword prev, then decrement index by action’s value. Then, based on the value of overflow:

cycle

If index is less than 0 or greater than maximum index, set index to maximum index.

cycle-on

If index is less than 1 or greater than maximum index, set index to maximum index.

sticky

If index is less than 0, set index to 0.

If index is greater than maximum index, set index to maximum index.

  1. If states is a list of idents, and index is the index of one of the entries in that list, set t’s value to that entry.

    Otherwise, set value to index.

  2. If t does not match 0, given states, and group is true, then set the value of all other toggles in the same toggle group as t to 0.

Define in much greater precision what it means to "become activatable". The element must become focusable (with a default spot in the focus order) and become capable of being activated by mouse/keyboard/etc. Similarly define the "already activatable" prose in more detail; we want to exclude things like text inputs, which would confuse a11y tooling, but include buttons that aren’t, like, submit buttons.

3.4. Creating and Activating Toggles Simultaneously: the toggle shorthand

Name: toggle
Value: <'toggle-root'>
Initial: see individual properties
Applies to: see individual properties
Inherited: see individual properties
Percentages: see individual properties
Computed value: see individual properties
Animation type: see individual properties
Canonical order: per grammar

While some cases require setting up a toggle on an ancestor of the elements that will activate and respond to the toggle, in many cases the scope rules for toggles are such that it’s fine to create the toggle on the element intended to activate the toggle as well.

The toggle shorthand sets both the toggle-root and toggle-trigger properties on an element together. The entire value of the property is assigned to toggle-root, while toggle-trigger is assigned to just the <custom-ident>s specified in the list, if any.

For example, in the following code for an spoiler-text element, the show/hide button precedes the content it will show and hide, so we can just create the toggle on it as well:
<spoiler-text>
  <summary>...</summary>
  <content>...</content>
</spoiler-text>
<style>
  spoiler-text > summary {
    toggle: show;
  }
  spoiler-text > content {
    toggle-visibility: toggle show;
  }
</style>

3.5. Accessibility Implications of Toggles

TODO

4. Selecting Elements Based on Toggle Value: the :toggle() pseudo-class

[SELECTORS-4] defines the :checked pseudo-class, which allows author to match certain elements (as defined by the host language) depending on their "checked" state.

Toggles provides a very similar functionality, allowing elements to be selected based on their toggle value, the :toggle() pseudo-class:

:toggle( <custom-ident> <toggle-value>? )

An element matches :toggle() if the element is in scope for a toggle with the name given by <custom-ident>, and either the toggle matches the provided <toggle-value>, or the <toggle-value> is omitted and the toggle is in any active value.

For example, if a toggle named "used" is defined on each element in an recipe’s ingredient list, the ingredients can respond to being clicked on easily:
.ingredient {
  toggle: used;
}
.ingredient:toggle(used) {
  color: silver;
  text-decoration: line-through;
}
A checkbox that can represent an "indeterminate" value might have two active values.
.tristate-check {
  toggle: check 2 at 0;
}
.tristate-check:toggle(check 1) {
  /* "checked" styles */
}
.tristate-check:toggle(check 2) {
  /* "indeterminate" styles */
}
While :not(:toggle(foo)) will correctly match an element whose "foo" toggle is in an inactive value, it will also match any element that doesn’t see a "foo" toggle at all.

If this is inconvenient, only elements that actually know about a particular toggle can be targeted by specifying the inactive value specifically:

.card:toggle(show 0) {
  /* Definitely *not* shown */
}

5. Automatically Hiding With A Toggle

The content-visibility property allows an element to suppress the layout and rendering of its contents, similar to display: none; however, its auto value allows the contents to still be visible to various searching and accessibility features, like find-in-page, hash-navigation, or tab order, and then to automatically become visible when the element becomes relevant to the user.

A common use-case for toggles is also to control whether an element is shown or hidden, and in many cases it would also be useful to allow the contents of elements "hidden" in this way to be accessible in the same ways. To allow for this, the toggle-visibility property allows an element to both respond to and control a toggle with its visibility.

Name: toggle-visibility
Value: normal | toggle <custom-ident>
Initial: normal
Applies to: all elements
Inherited: no
Percentages: n/a
Computed value: as specified
Canonical order: per grammar
Animation type: not animatable

The toggle-visibility property allows an element to automatically tie its display to a particular toggle bi-directionally, while still exposing its contents to be focused, found-in-page, etc., automatically showing itself when relevant.

normal

The property has no effect.

toggle <custom-ident>

If the element is in scope for a toggle whose name matches the <custom-ident>, and that toggle is in the inactive value, then the element acts as if content-visiblity: auto is specified, except that being on-screen does not make it relevant to the user.

If the element starts being relevant to the user, it fires a toggle activation, with a name of the given <custom-ident> and a action of set 1.

Note: If the toggle is in an active value, or the element can’t see the specified toggle at all, the element renders normally.

For example, an "accordion" display, such as used for a page of FAQs, can use toggle-visibility to hide the answers by default, but still make them accessible to find-in-page:
<dl class=accordion>
  <dt>Question 1?
  <dd>Long answer......
  <dt>Question 2?
  <dd>Another long answer.....
</dl>
<style>
  .accordion > dt {
    toggle: show;
  }
  .accordion > dd {
    toggle-visibility: toggle show;
  }
</style>

Each dt establishes a separate "show" toggle, with all the defaults filling in to produce a standard "checkbox"-like behavior, all initially in the inactive value. Each dt can be activated to show or hide the following answer.

Each dd can see the toggle established by its preceding dt, and will start out not rendering. If the user searches on the page for a term, or visits the page from a link with an #anchor linking into one of the answers, the relevant answer will automatically activate the toggle, causing the dd to become visible.

Define ordering of activations, so if multiple elements become relevant at the same time and they’re all part of a toggle group, which one "wins" is well-defined.

6. Scripting API

6.1. CSSToggleMap

partial interface Element {
  [SameObject] readonly attribute CSSToggleMap toggles;
};

interface CSSToggleMap {
  maplike<DOMString, CSSToggle>;
  CSSToggleMap set(DOMString key, CSSToggle value);
};

The toggles attribute on Elements represents the established toggles on the element: each toggle is represented by an entry in the map, with its key equal to its name and its value a CSSToggle representing the rest of the toggle’s data.

Entries can be automatically added to toggles by the toggle-root property when update the rendering occurs, if toggle-root defines a toggle specifier whose name doesn’t exist in toggles yet; see § 3.1 Toggle Creation Details for details.

The set(key, value) method steps are:
  1. If key is an ASCII case-insensitive match for "none", throw a SyntaxError.

  2. If value’s [[ToggleMap]] internal slot is non-undefined, then let oldMap be the map entries of the slot’s value, and remove the entry from oldMap whose value is value.

  3. Set value’s [[ToggleMap]] internal slot to this.

  4. Let map be this’s map entries. Set map[key] to value.

  5. Return this.

Note: A given CSSToggle can only live in one CSSToggleMap at a time. This avoids some otherwise confusing or ambiguous situations.

6.2. CSSToggle

interface CSSToggle {
  attribute (unsigned long or DOMString) value;
  attribute unsigned long? valueAsNumber;
  attribute DOMString? valueAsString;

  attribute (unsigned long or FrozenArray<DOMString>) states;
  attribute boolean group;
  attribute CSSToggleScope scope;
  attribute CSSToggleCycle cycle;

  constructor(optional CSSToggleData options);
};

dictionary CSSToggleData {
  (unsigned long or DOMString) value = 0;
  (unsigned long or sequence<DOMString>) states = 1;
  boolean group = false;
  CSSToggleScope scope = "wide";
  CSSToggleCycle cycle = "cycle";
};

enum CSSToggleScope {
  "narrow",
  "wide",
};

enum CSSToggleCycle {
  "cycle",
  "cycle-on",
  "sticky",
};

A CSSToggle represents a toggle; changing any attribute on the CSSToggle will change the corresponding items of the toggle it represents, and vice versa.

Note: Only the value will change directly on a toggle and get reflected back to the CSSToggle; the rest of its items are fixed on creation unless manually changed on its associated CSSToggle.

A CSSToggle will usually be an established toggle on an element, present in the element’s toggles attribute, but can also be used independently. This is tracked by a [[ToggleMap]] internal slot, containing either undefined (the initial value) or a weak reference to the CSSToggleMap the CSSToggle is in.

The new CSSToggle(options) constructor steps are:
  1. For each namevalue in options:

    1. If name is "states" and value is a list:

    2. Set the internal slot named name of this to value.

  2. Return this.

The valueAsNumber getter steps are:
  1. If this’s value slot contains an integer, return that.

  2. If this’s value slot contains a string, this’s states internal slot is a list, and the string is present in that list, return the index of the first instance of that string in the list.

  3. Otherwise, return null.

The valueAsNumber setter steps are:

  1. If the given value is an integer, update the value slot of this to the given value and return.

  2. Otherwise, throw a TypeError exception.

The valueAsNumber attribute does not have a corresponding internal slot.

The valueAsString getter steps are:
  1. If this’s value internal slot contains an string, return that.

  2. If this’s value internal slot contains an index, and this’s states internal slot is a list and has an entry corresponding to that index, return that entry.

  3. Otherwise, return null.

The valueAsString setter steps are:

  1. If the given value is an string, update the value slot of this to the given value and return.

  2. Otherwise, throw a TypeError exception.

The valueAsString attribute does not have a corresponding internal slot.

The value setter steps are to update the value slot of this to the given value.
To update the value slot of a CSSToggle this to a new value val:
  1. If this is an established toggle, then change a toggle this, with an action of set followed by val.

    Note: Due to the reflection, this will automatically update the value internal slot of this as well.

  2. Otherwise, set the value internal slot of this to val.

The states setter steps are:
  1. If the given value is a sequence of DOMStrings:

  2. Set this’s states internal slot to the given value.

6.3. CSSToggleEvent

interface CSSToggleEvent : Event {
  constructor(DOMString type, optional CSSToggleEventInit eventInitDict = {});
  readonly attribute DOMString toggleName;
  readonly attribute CSSToggle? toggle;
};

dictionary CSSToggleEventInit : EventInit {
  DOMString toggleName = "";
  CSSToggle? toggle = null;
};

whatwg/dom#600 means apparently events don’t yet work if their init dictionaries have required members. Neither of these have reasonable defaults, especially the toggle part, but oh well I guess.

To fire a toggle change event at an Element el for a toggle toggle means to fire an event named "togglechange" using CSSToggleEvent, setting toggleName to toggle’s name and toggle to the entry of el’s toggles map with toggle’s name as the key.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Advisements are normative sections styled to evoke special attention and are set apart from other normative text with <strong class="advisement">, like this: UAs MUST provide an accessible alternative.

Conformance classes

Conformance to this specification is defined for three conformance classes:

style sheet
A CSS style sheet.
renderer
A UA that interprets the semantics of a style sheet and renders documents that use them.
authoring tool
A UA that writes a style sheet.

A style sheet is conformant to this specification if all of its statements that use syntax defined in this module are valid according to the generic CSS grammar and the individual grammars of each feature defined in this module.

A renderer is conformant to this specification if, in addition to interpreting the style sheet as defined by the appropriate specifications, it supports all the features defined by this specification by parsing them correctly and rendering the document accordingly. However, the inability of a UA to correctly render a document due to limitations of the device does not make the UA non-conformant. (For example, a UA is not required to render color on a monochrome monitor.)

An authoring tool is conformant to this specification if it writes style sheets that are syntactically correct according to the generic CSS grammar and the individual grammars of each feature in this module, and meet all other conformance requirements of style sheets as described in this module.

Partial implementations

So that authors can exploit the forward-compatible parsing rules to assign fallback values, CSS renderers must treat as invalid (and ignore as appropriate) any at-rules, properties, property values, keywords, and other syntactic constructs for which they have no usable level of support. In particular, user agents must not selectively ignore unsupported component values and honor supported values in a single multi-value property declaration: if any value is considered invalid (as unsupported values must be), CSS requires that the entire declaration be ignored.

Implementations of Unstable and Proprietary Features

To avoid clashes with future stable CSS features, the CSSWG recommends following best practices for the implementation of unstable features and proprietary extensions to CSS.

Non-experimental implementations

Once a specification reaches the Candidate Recommendation stage, non-experimental implementations are possible, and implementors should release an unprefixed implementation of any CR-level feature they can demonstrate to be correctly implemented according to spec.

To establish and maintain the interoperability of CSS across implementations, the CSS Working Group requests that non-experimental CSS renderers submit an implementation report (and, if necessary, the testcases used for that implementation report) to the W3C before releasing an unprefixed implementation of any CSS features. Testcases submitted to W3C are subject to review and correction by the CSS Working Group.

Further information on submitting testcases and implementation reports can be found from on the CSS Working Group’s website at http://www.w3.org/Style/CSS/Test/. Questions should be directed to the public-css-testsuite@w3.org mailing list.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-CONTAIN-2]
Tab Atkins Jr.; Florian Rivoal; Vladimir Levin. CSS Containment Module Level 2. URL: https://drafts.csswg.org/css-contain-2/
[CSS-DISPLAY-3]
Tab Atkins Jr.; Elika Etemad. CSS Display Module Level 3. URL: https://drafts.csswg.org/css-display/
[CSS-POSITION-3]
Elika Etemad; Tab Atkins Jr.. CSS Positioned Layout Module Level 3. URL: https://drafts.csswg.org/css-position/
[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. URL: https://drafts.csswg.org/css-values-4/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[SELECTORS-4]
Elika Etemad; Tab Atkins Jr.. Selectors Level 4. URL: https://drafts.csswg.org/selectors/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Property Index

Name Value Initial Applies to Inh. %ages Ani­mat­able Anim­ation type Canonical order Com­puted value Media
toggle <'toggle-root'> see individual properties see individual properties see individual properties see individual properties see individual properties per grammar see individual properties
toggle-group none | [ <custom-ident> self? ]# none all elements no n/a no per grammar as specified interactive
toggle-root none | <toggle-specifier># none all elements no n/a no per grammar as specified interactive
toggle-trigger none | <toggle-trigger># none elements without existing activation behavior (see prose) no n/a no per grammar the keyword none, or a list of <toggle-trigger> values interactive
toggle-visibility normal | toggle <custom-ident> normal all elements no n/a not animatable per grammar as specified

IDL Index

partial interface Element {
  [SameObject] readonly attribute CSSToggleMap toggles;
};

interface CSSToggleMap {
  maplike<DOMString, CSSToggle>;
  CSSToggleMap set(DOMString key, CSSToggle value);
};

interface CSSToggle {
  attribute (unsigned long or DOMString) value;
  attribute unsigned long? valueAsNumber;
  attribute DOMString? valueAsString;

  attribute (unsigned long or FrozenArray<DOMString>) states;
  attribute boolean group;
  attribute CSSToggleScope scope;
  attribute CSSToggleCycle cycle;

  constructor(optional CSSToggleData options);
};

dictionary CSSToggleData {
  (unsigned long or DOMString) value = 0;
  (unsigned long or sequence<DOMString>) states = 1;
  boolean group = false;
  CSSToggleScope scope = "wide";
  CSSToggleCycle cycle = "cycle";
};

enum CSSToggleScope {
  "narrow",
  "wide",
};

enum CSSToggleCycle {
  "cycle",
  "cycle-on",
  "sticky",
};

interface CSSToggleEvent : Event {
  constructor(DOMString type, optional CSSToggleEventInit eventInitDict = {});
  readonly attribute DOMString toggleName;
  readonly attribute CSSToggle? toggle;
};

dictionary CSSToggleEventInit : EventInit {
  DOMString toggleName = "";
  CSSToggle? toggle = null;
};

Issues Index

Define the precise point in update the rendering when toggles are created if toggle-root names a toggle that doesn’t exist on the element yet.
Radio buttons have one particular tabbing/switching/activation behavior (they occupy a single tabindex spot; once reached you can move between them with arrow keys; moving to one auto-activates it), but not all groups will want that behavior. Accordions, in particular, probably don’t, and instead just want to effectively be independent checkboxes that happen to only have one active at a time.

We probably want to add a bool to toggle groups dictating this; are there more than these two behaviors to deal with?

Consider adding support for an at-rule that defines machine states, the available events to trigger from each state, and the resulting state of each event. In that case <trigger-action> would need to also accept custom events, possibly marked by a new keyword or function.
Define in much greater precision what it means to "become activatable". The element must become focusable (with a default spot in the focus order) and become capable of being activated by mouse/keyboard/etc. Similarly define the "already activatable" prose in more detail; we want to exclude things like text inputs, which would confuse a11y tooling, but include buttons that aren’t, like, submit buttons.
TODO
Define ordering of activations, so if multiple elements become relevant at the same time and they’re all part of a toggle group, which one "wins" is well-defined.
whatwg/dom#600 means apparently events don’t yet work if their init dictionaries have required members. Neither of these have reasonable defaults, especially the toggle part, but oh well I guess.