Archived:
❌ This feature has been abandoned (at least for now). Our notes are likely out-of-date.
See the Unofficial Draft specification for details.
Authors ¶
- Miriam Suzanne
- Tab Atkins Jr.
See the References & Acknowledgements for additional contributors and prior art
Participate ¶
- Unofficial draft specification
- Github issues for draft spec
- CSSWG tracking issue
- OddBird JS polyfill/prototype (and demo)
Introduction ¶
There are many use-cases
where an interaction (click/gesture) on one element
toggles a ‘state’ that can be shared with other elements.
This can range from toggling light/dark mode,
to activating slide-in navigation,
opening and closing menus,
or interacting with sectioned content
as tabs, accordions, or carousels.
HTML also provides the <input type=checkbox>
element,
with a “checked” state that toggles between true
and false
when the user activates the element,
and which can be selected by the :checked
pseudoclass.
These cases currently require custom Javascript, which adds a barrier for authors otherwise capable of implementing the visual design – and often results in less performant, less accessible solutions.
We propose generalizing the pattern so that it can be applied to any element using a declarative syntax in CSS, with built-in accessibility and performance.
Goals [or Motivating Use Cases, or Scenarios] ¶
To borrow language from Nicole Sullivan:
a gesture on a trigger causes a state change on a target
Where:
- a gesture is usually some form of user interaction, like click/enter activation, scrolling, etc. It may also be useful to consider non-user ‘gestures’ such as animation events that trigger a toggle state.
- a trigger and a target are both elements in the DOM (often different elements, but sometimes the same element)
- a state change can be moving through a list of possible states, or setting a particular state
Several of the expected use-cases involve showing and hiding content based on the state of a toggle:
- tabs
- accordions / tree views
- carousels
- popups
- dialogs
- summary/details
- off-canvas menus
However, there are also use-cases for a toggle to have more complex style impact:
- light/dark/high-contrast/auto color themes
- adjusting font sizes
- adjusting compact/comfortable spacing
- adjusting ‘views’, eg calendar/agenda or horizontal/vertical layout
The Open UI project has been working on new elements/attributes that could help with some of these use-cases at a high level – such as panel sets (‘spicy sections’), selectmenus, and popups. It’s our goal that CSS toggles provide some shared functionality that those elements & attributes can rely on, while also giving authors access to define new toggle-based patterns.
Along the way, we have a few goals for how this feature should work:
- Accessibility should be handled internally whenever possible, rather than relying on author intervention
- Relationships between a trigger and its impact should be established and accessed in CSS, rather than relying on unique IDs or selectors-as-property-values.
- JS should not be required in the most common use-cases
- Defaults should help facilitate the most common use-cases, while still allowing more complex state interactions.
Non-goals ¶
Managing application state ¶
While there is a much larger need for improved application state management on the web, this proposal is focused specifically on the needs of CSS. That line is not necessarily clean and simple to define – but we think that:
- State changes which need to update the DOM, or be ‘saved’ into app state should be primarily handled outside CSS. Those are not presentation-specific concerns.
- It should still be possible to use this feature in tandem with other web languages to represent/display those more complex application states in CSS.
For example, interacting with a ‘tab’ or ‘accordion’ design pattern is a purely presentational concern that requires some state management. However, changing the ‘status’ of a user/page from ‘logged out’ to ‘logged-in’ – or deleting an item from a list – are broader application states that may only be ‘reflected’ in the presentation.
The boundaries are complicated, since it may be useful for eg:
- Activating a
save
button changes a toggle state tosaving
- The styles are able to reflect that state change immediately
- JS can listen for either the button activation, or the state change, in order to trigger a network request and/or change app state internally
- JS can then trigger a change in the CSS toggle state when the internal app data updates are complete
That means we need both:
- a CSS syntax that allows for handling the presentational needs
- an API allowing other web languages to read & set CSS states when necessary
Potential name confusion ¶
The term ‘toggle’ has been used in reference to various different features over the years.
CSS Values & Units level 5
defines an (unimplemented) toggle()
function that allows
cycling through a set of values as elements are nested.
For example, toggling between italic
/normal
or cycling through different list markers:
em { font-style: toggle(italic; normal); }
ul { list-style-type: toggle(disc; circle; square; box); }
This proposal is unrelated to that CSS function.
(We might want to bikeshed the naming of one or the other).
David Baron has proposed cycle()
as an alternative name for the toggle()
function.
The word is also sometimes used in reference to ‘switch’ components. While this proposal has some overlap with a switch – it could be used to generate the switch behavior – we are not attempting to define a new element here.
It’s also possible that the term ‘toggle’ could cause confusion, as many might expect it to have a boolean (on/off) behavior. The current proposal, however, allows for any number of desired states.
Proposal for declarative CSS toggles ¶
Broad overview ¶
This proposal relies on several primary concepts. First off the toggles themselves:
- Every toggle has a name, a current state, set number of possible states, and some metadata for determining how to move between states by default.
- Any element can become a toggle root
for any number of toggles,
using the
toggle-root
property.
Once we have toggles, we need a way to access them – both for the sake of changing state, and also in order to use that state somehow. Every toggle has a toggle scope of elements that are able to ‘see’ or alter its state. That scope includes the toggle-root element and any descendants (similar to inheritance), as well as (optionally) siblings and the descendants of siblings (similar to the css counters).
Any element that can ‘see’ and access a given toggle:
- Can become a trigger for that toggle
using the
toggle-trigger
property, such that interactions on the trigger element update the state of the toggle. - Can use the
:toggle()
pseudo-class to select based on the current state of the toggle
It’s important for authors to understand the distinctions between these things:
- The toggle root element, where a toggle is ‘hosted’
- The toggle scope, where a toggle is ‘visible’
- Any number of toggle trigger elements, which can update the toggle state when activated
Establishing toggles (toggle-root
) ¶
The toggle-root
property
generates new toggles on a given element,
and controls how those toggles behave.
The overall syntax allows either none
,
or a comma-separated list of toggle definitions.
A simple auto/light/dark color-mode toggle might be defined on the document root:
:root {
/* color-mode toggle with 2 active states */
toggle-root: color-mode 2;
}
Warning:
We could consider adding longhand properties
for the various parts of the toggle description,
and make toggle-root
into a shorthand.
But that’s not a simple request for implementors,
since it would involve
collating multiple list-valued properties.
Toggle names ¶
Each toggle definition begins with a (required) toggle name – an identifier that allows us to access this particular toggle.
Toggle states ¶
After the name, we can optionally define the initial and known states. Toggle states are represented as either integers or custom idents:
<<toggle-states>> = <<known-states>> [at <<toggle-state>>]?
<dfn><<known-states>></dfn> = <<integer [1, ∞]>> | '[' <<custom-ident>>* ']'
<dfn><<toggle-state>></dfn> = <<integer [0, ∞]>> | <<custom-ident>>
- Known states can be given as either
an integer (the maximum active state
[1,∞]
, with 0 being inactive) or a bracketed list of state names (in which case the first state is considered ‘inactive’ and the maximum is determined by list length minus 1). The default is1
, meaning the toggle has a single active state. - The initial state can also be given as either an integer
(including 0 for inactive)
or a custom-ident.
The default is
0
, meaning the toggle is inactive when created.
When known states are provided as a list of names, those states can be set & accessed either by name (as specified) or by number (0-indexed list position). Toggles are also allowed to be put in states that are ‘unknown’ above the maximum, or with a name that is not listed.
:root {
/* 2 active states (initially inactive) */
toggle-root: color-mode 2;
/* same result as above, but with explicit initial state */
toggle-root: color-mode 2 at 0;
}
.my-toggle {
/* 4 active states (initially in 2nd active state) */
toggle-root: my-toggle 4 at 2;
}
Warning:
Note that this syntax has changed since the initial draft spec & explainer.
Named states provide more clarity around the purpose of a state beyond simple numbering:
html {
toggle-root:
colors [auto light dark] at light,
middle-out 10 at 5,
switch /* `1 at 0` is the default */
;
}
Note:
The Toggles Polyfill currently supports:
- the previously specified
initial/max
syntax - the new keyword
max at initial
syntax - the bracket/keyword
[one two three] at three
named-state syntax
Default toggle events & ‘overflow’ ¶
While it’s possible to define more complex events to move from one specific state to another – as you might in more complex state machines – many of the known use-cases can be handled with simple increment/decrement events.
To facilitate these simpler use-cases, every toggle has a pre-defined ‘overflow’ behavior when incrementing above, or decrementing below the list of known states:
Warning:
These options have changed since the initial draft spec and explainer.
- cycle (default):
Increments above the maximum state return to
0
(inactive), and decrements below0
will return to the maximum state. - cycle-on:
Increments above the maximum state return to
1
(first active), and decrements below1
will return to the maximum state. This is the same as ‘cycle’, but always maintains an active toggle. - sticky: state will increment until reaching the maximum, and then stay at the maximum until given more explicit direction.
Unknown states are considered both ‘higher than the maximum’ and ‘lower than the minimum’ so that overflow rules are applied in either direction. Increments cycle to the minimum (0 or 1), while decrements cycle to the maximum state.
Toggle groups and scopes ¶
Finally, there are two optional boolean keywords, for grouping and scoping toggles:
- group: boolean (
false
if omitted) indicates if this toggle is part of a ‘toggle group’ using the same name - self: boolean (
false
if omitted) optionally narrows ‘toggle scope’ (what elements ‘see’ the toggle) to descendant elements (narrow scope) – otherwise a toggle is also visible to following siblings & their descendants (wide scope)
The default ‘wide’ scope is similar
to the way CSS counters behave,
while the ‘narrow’ (self
) scope
is more similar to inheritance.
Establishing toggle triggers (toggle-trigger
) ¶
Once a toggle has been established,
any elements inside the scope of that toggle
can be set as ‘triggers’ for that scope,
using the toggle-trigger
property.
Triggers would become activatable in the page
(part of the focus order, and able to receive interaction) –
so that user-activation of the trigger element
increments the state of one or more toggles.
ToDo:
Need to define what it means to ‘become activatable’. By default, the host language (such as HTML) can provide a basic definition. In future iterations (level 2 of the spec), we might also want to consider more explicit options such as gestures and scroll-triggers, beyond simple click/keyboard activation defined in HTML.
The toggle-trigger
property can be set to none
,
or a comma-separated list of one or more
toggle-activation instructions.
Each instruction includes:
- The name of the toggle to activate
- An (optional) event for the toggle
When a trigger is activated by a user, then for each toggle listed, if a toggle of that name is visible to the trigger, its state is updated according to the specified event (if valid). There are currently several event types provided:
- Increment (default) using the
next
keyword followed by an optional non-zero integer (the number of steps to increment, defaulting to 1). If no event is specified, the default is an increment of 1 step. - Decrement using the
prev
keyword followed by an optional non-zero integer (the number of steps to decrement, defaulting to 1). If not event is specified, the default is an increment of 1 step. - Set an explicit state by using the
set
keyword followed by any valid state name or number.
To trigger the color-mode toggle created above, we could add triggers anywhere in the page (since the toggle scope is the entire document in this case). Triggers could either cycle the value, or set it to a particular state. The current spec allows these two formats:
button[toggle-colors] {
/* on activation: increment toggle to next state along default path */
toggle-trigger: color-mode;
}
button[toggle-colors='dark'] {
/* on activation: set toggle to a specific state */
toggle-trigger: color-mode set dark;
/* named states can be referred to by either name or position */
toggle-trigger: color-mode set 2;
}
Increment and decrement events allow us to move in either direction around a slideshow or carousel:
.next {
/* incrementing would be the default */
toggle-trigger: slide;
toggle-trigger: slide next;
}
.previous {
toggle-trigger: slide prev;
}
There is much more to explore along these lines, especially if we want to allow for state-machine ‘transitions’ that define dynamic state changes. I think that would make sense as a level 2 extension of this proposal, and a case that we should consider while designing the syntax.
Combined root and trigger shorthand (toggle
) ¶
While it can be useful to have them separate,
there are many use-cases where the same element
can act as both root and trigger for a toggle.
The toggle
shorthand property
has the same syntax as toggle-root
,
but establishes the element as both root and trigger.
For example, a definition list:
<dl class='accordion'>
<dt>Term 1</dt>
<dd>Long description......</dd>
<dt>Term 2</dt>
<dd>Another long description.....</dd>
</dl>
Could set each term as both a toggle root and a trigger for its own toggle:
.accordion > dt {
toggle: glossary;
}
Note:
When multiple toggles have the same name
and overlapping scope,
an element will only see the ‘closer’ toggle –
so each glossary
toggle in the example
is only available to the dd
element that comes immediately after
(before another glossary
toggle is defined)
Toggling visibility (toggle-visibility
) ¶
One of the common use-cases for this feature
is the ability to build various types of
‘disclosure widget’ –
from tabs and accordions, to popups, and details/summary.
It’s essential that we make that
both simple to achieve and also accessible by default.
In many cases,
we want this ‘off-screen’ (temporarily hidden) content
to remain available for accessibility features,
in-page searching, linking, focus, etc.
The toggle-visibility
property
allows an element to automatically tie its display
to the state of a particular toggle.
Note:
The existing
content-visibility
property
provides an auto
value,
allowing the contents to remain accessible
to various searching and accessibility features
like find-in-page, hash-navigation, or tab order,
when hidden from rendering –
but then to automatically become visible
when the element becomes relevant to the user.
The toggle-visibility
property
would work similarly.
In addition to changing visibility based on the toggle state, this allows us to change the toggle based on its visibility. If a currently-hidden element becomes ‘relevant to the user’ (through linking, search, etc) then the toggle is set to an active state, and the content is displayed.
The spec currently allows normal
(no effect)
or toggle <toggle-name>
values.
Using our definition-list example above,
we can add visibility-toggling to the definitions –
hidden by default,
but connected to toggle state
and available when relevant:
.accordion > dt {
toggle: glossary;
}
.accordion > dd {
toggle-visibility: toggle glossary;
}
Question:
Should we extend this syntax to allow
associating an element toggle-visibility
with a particular active state?
Something like
toggle-visibility: toggle tabs 3;
could associate the visibility of an element
with the 3rd active state of the tabs
toggle.
See the toc-style tab markup
use-case, for example.
Note:
The Toggles Polyfill currently implements
toggle-visibility
by simply applying display:none
without any of the expected benefits
that a browser implementation might provide.
Selecting based on toggle state (:toggle()
) ¶
While toggling visibility is common,
there are often associated styles based on the same state
(e.g. styling the active tab, when its contents are visible) –
or toggles that don’t relate to visibility at all.
The :toggle()
functional pseudo-class
allows us to select elements based on the state of a toggle
(as long as the element is in that toggle’s scope)
and style based on the toggle state.
The function itself requires a toggle-name, and also accepts an optional integer for selecting on specific active states when there are multiple.
With our auto/light/dark mode example, we can apply the color theme on the root element, using either named or numbered states:
html { /* auto settings, using media queries */ }
html:toggle(color-mode 1) {
/* light mode color settings */
}
html:toggle(color-mode dark) {
/* dark mode color settings */
}
We can also add active styling to the triggers:
button[toggle-colors='dark']:toggle(color-mode dark) {
border-color: cyan;
}
Grouping exclusive toggles (toggle-group
) ¶
Toggles can also be grouped together
using the toggle-group
property –
in which case only one toggle from the group
can be ‘active’ at a time.
This is similar to how radio inputs work in HTML, but would also be useful in describing patterns like tabs or exclusive accordions.
See the ‘tab and accordion toggle-groups’ section for a full example of this use-case.
Note:
Toggle groups are not the same as ‘focus groups’ proposed elsewhere. The former impacts how multiple toggles relate (one active at a time), while the latter allows multiple triggers to be grouped in the tab order (one tab-stop for all interactions). Existing HTML radio input behavior would require both features – grouping both the trigger focus and states.
Question:
Would it be possible to address focus-grouping from CSS, as part of this proposal, or does that focus-management require an HTML attribute?
Javascript API for CSS toggles ¶
It’s important that toggle states are both ‘available to’ JavaScript, and can also be ‘manipulated by’ scripting.
The details still need to be fleshed out, but roughly we would want to:
- Expose a map of toggles on an element, associating each toggle name with the details of the toggle
- Ability to create toggles manually
- Ability to delete toggles manually
- Ability to set toggle state manually
I expect each state of a toggle would have an optional name and number value exposed – where predefined states have both a name & number, but any trigger-defined states only have one or the other (as defined by the trigger).
ToDo:
Needs details.
Key scenarios ¶
ToDo:
Copy earlier examples into this section, and flesh out additional use-cases.
Binary switch ¶
We can recreate the basic behavior of a self-toggling checkbox or switch component:
.switch {
toggle: switch;
}
.switch:toggle(switch) {
/* style the active state */
}
Note:
See the binary switch demo.
Details and accordion disclosure components ¶
The behavior of a details/summary element can be replicated:
summary {
toggle: details;
}
details > :not(summary) {
toggle-visibility: toggle details;
}
This can be extended to a whole list of elements, to create a non-exclusive accordion:
<dl class='accordion'>
<dt>Term 1</dt>
<dd>Long description......</dd>
<dt>Term 2</dt>
<dd>Another long description.....</dd>
</dl>
Where each term toggles the following definitions:
.accordion > dt {
toggle: glossary;
}
.accordion > dd {
toggle-visibility: toggle glossary;
}
Note:
See the accordion demo.
Both of these examples rely on the toggled content following the trigger element. By moving the toggle-root to a wrapping element we can avoid that restriction. With this code, the summary is no longer required to come first:
details {
toggle-root: details;
}
summary {
toggle-trigger: details;
}
details > :not(summary) {
toggle-visibility: toggle details;
}
Color-mode preferences ¶
It is very common for sites to support both light and dark ‘modes’ for a site, and provide a toggle between those modes. Some sites also provide ‘auto’ mode (the result of a user-preference) and/or additional modes like ‘high-contrast’.
This use-case could be handled with a toggle on the root element. In this case I’m using the proposed syntax for named states:
html {
toggle-root: mode [auto light dark];
}
html:toggle(mode light) {
/* colors for light mode */
}
html:toggle(mode dark) {
/* colors for dark mode */
}
.mode-btn {
toggle-trigger: mode;
}
Note:
See the named-modes demo, and a similar named-colors demo with individual triggers for each state.
Tree views ¶
A tree view can be created by nesting the accordion/disclosure pattern:
<ul>
<li><a href='#'>home</a></li>
<li>
<button class='tree'>resources</button>
<ul>
<li><a href='#'>articles</a></li>
<li><a href='#'>demos</a></li>
<li>
<button class='tree'>media</button>
<ul>
<li><a href='#'>audio</a></li>
<li><a href='#'>visual</a></li>
</ul>
</li>
</ul>
</li>
</ul>
And applying the show/hide behavior at every level:
.tree {
toggle: tree;
}
.tree + ul {
toggle-visibility: toggle tree;
}
Note:
See the tree-view demo.
Tab and exclusive-accordion toggle-groups ¶
Given the following HTML (similar to the proposed ‘spicy sections’ element):
<panel-set>
<panel-tab>first tab</panel-tab>
<panel-card>first panel content</panel-card>
<panel-tab>second tab</panel-tab>
<panel-card>second panel content</panel-card>
</panel-set>
We can define the exclusive/grouped behavior using toggles:
panel-set {
/* The common ancestor establishes a group */
toggle-group: tab;
}
panel-tab {
/* Each tab creates a sticky 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 sticky;
}
panel-tab:first-of-type {
/* The first tab also sets its initial state
to be active */
toggle: tab 1 at 1 group sticky;
}
panel-tab:toggle(tab) {
/* styling for the active tab */
}
panel-card {
/* card visibility is linked to toggle state */
toggle-visibility: toggle tab;
}
The same CSS works, even if additional wrappers are added around each tab/card pair:
<panel-set>
<panel-wrap>
<panel-tab>first tab</panel-tab>
<panel-card>first panel content</panel-card>
</panel-wrap>
<panel-wrap>
<panel-tab>second tab</panel-tab>
<panel-card>second panel content</panel-card>
</panel-wrap>
</panel-set>
Note:
See the panelset demo.
Tabs using table-of-contents code order? ¶
In order to properly layout tabs as a group above the panel contents, it’s common for tab components use a table-of-contents approach to the markup:
<panel-set>
<tab-list>
<panel-tab>first tab</panel-tab>
<panel-tab>second tab</panel-tab>
</tab-list>
<card-list>
<panel-card>first panel content</panel-card>
<panel-card>second panel content</panel-card>
</card-list>
</panel-set>
This complicates things, since we can no longer rely on the flow of toggle-scopes to associate each trigger with an individual iteration of the toggle.
The rough behavior is still possible to achieve, using a single toggle with multiple active states, but it requires somewhat explicit nth-of-type/nth-child selectors:
/* shared toggle with an active state for each tab-panel */
panel-set {
toggle-root: tabs <tab-count> at 1 cycle-on;
}
/* each tab sets an explicit state */
panel-tab:nth-child(<tab-position>) {
toggle-trigger: tabs <tab-position>;
}
/* each panel responds to an explicit state */
panel-card:nth-child(<tab-position>):toggle(tabs <tab-position>) {
display: block;
}
There are several ways
we might be able to improve on this.
Most important, perhaps,
we could consider extending toggle-visibility
to accept not only a toggle name,
but also a specific active state.
Question:
Can we also improve on
the bulky/repetitive selector logic here?
That seems like it would require
a feature more like the sibling-count()
/sibling-index()
functions proposed elsewhere.
Carousels and slide-shows? ¶
The current spec doesn’t have great support for carousel-like design patterns, but it wouldn’t take much to improve the basics. Let’s imagine the following html structure:
<section>
<article>…</article>
<article>…</article>
<article>…</article>
<article>…</article>
</section>
We can add a toggle to track the state of the carousel:
section {
/* 4 is the number of slides */
/* sticky behavior starting at 1 ensures a slide is always active */
toggle-root: slides 4 at 1 sticky
}
From here, we’re in a similar situation to the
‘table-of-contents’ example above.
Ideally we would want some way to tie
article
visibility to the specific state of our toggle.
Failing that, we can do something like:
/* articles end up on the left */
article {
transform: translateX(var(--x, -100%));
transition: transform 300ms ease-out;
}
/* bring the active slide into view */
article:nth-child(<n>):toggle(slides <n>) {
--x: 0;
}
/* move upcoming slides to the right */
article:nth-child(<n>):toggle(slides <n>) ~ article {
--x: -100%;
}
For navigating the carousel,
both pagination controls and ‘next/prev slide’ triggers
would be straight-forward.
We just need to put them
anywhere visible to the section
element:
.next-slide {
toggle-trigger: slides next;
}
.prev-slide {
toggle-trigger: slides prev;
}
.to-slide-3 {
toggle-trigger: slides set 3;
}
In many cases, we would also want to control this carousel using scroll, in addition to (or instead of) buttons. That would require further integration with scroll/snapping behavior, which we could consider for level 2 of the spec.
Triggering dynamic transitions ¶
In many state machines, a given active state is able to describe a the named ‘events’ that are available for a trigger. Rather than having the trigger use a pre-defined event, such as prev/next/set, the trigger would choose one of several custom events allowed by the current state.
This may not be required in a first version of CSS toggles, but we should consider how/if the syntax could be extended to support this use-case.
My initial sense is that we could allow this sort of ‘state machine’ to be defined using an at-rule (name TBD):
@machine <machine-name> {
<state-1> {
<event-1>: <target-state>;
<event-2>: <target-state>;
}
<state-2> {
<event-1>: <target-state>;
<event-2>: <target-state>;
}
}
When creating a new toggle, it could be based on one of these named machines (syntax TBD):
html {
toggle-root: my-toggle machine(<machine-name>);
}
We could consider adding an setting
at the toggle-root level
to either enforce that all triggers
use named transitions (strict
),
or optionally allow triggers
to choose between states, transitions,
and default incrementing:
html {
toggle-root: my-toggle machine(<machine-name>, strict);
}
On the trigger side,
we would need a syntax
that clearly references a custom event name,
rather than a pre-defined event.
In this example, I use event
as the keyword –
actual syntax TBD:
.save {
/* trigger a named event, that defines target state */
toggle-trigger: my-toggle event save;
}
As an example, Adam Argyle posted this state machine diagram:
We could establish that as a machine for CSS toggles:
@machine fetch {
idle {
try: loading;
}
loading {
resolve: success;
reject: failure;
}
failure {
try: loading;
}
/* as a final state, 'success' does not have transitions */
}
I’ve reused the try
transition name in place of fetch
and retry
,
so that a single trigger can
activate either transition,
as long as the machine is either
in a ‘idle’ or ‘failure’ state:
form {
/* maybe name can be optionally implied by machine()? */
toggle-root: machine(fetch);
}
.try {
toggle-trigger: fetch event try;
}
Note:
All of this behavior is already possible
with the specified syntax,
since trigger actions can be re-defined
based on the current state –
however, the result is difficult to read
or define clearly in a single place.
Each trigger would need to
define it’s own transtions:
.try:not(:toggle(fetch loading)) { toggle-trigger: fetch set loading; }
.
Note:
See the
@machine
demo.
Allow trigger-defined/unknown states? ¶
Triggers can define arbitrary transitions between states, and are also able to define new states:
html {
/* no states defined */
toggle-root: page;
}
button.save {
/* triggers can define arbitrary states */
toggle-trigger: page set saving;
}
New states defined by a trigger do not have any number/name association, and fall ‘outside’ the default cycle behavior. They are considered active states, but ‘above the maximum’ for the sake of incrementing, and ‘below the minimum’ for the sake of decrementing.
Note:
There have been many discussions about how to express ‘indeterminate’ state in the current syntax – and in many ways this feature would be similar to allowing ‘indeterminate’ states.
Detailed Design Discussion ¶
Avoiding recursive behavior with toggle selectors ¶
In order to allow selector access to toggles
(using the :toggle()
functional pseudo-class)
without causing recursive behavior,
toggles exist and persist as independent state on a given element –
unaffected by any CSS properties.
CSS properties can only:
- create new toggles, and establish their initial state
- add or remove toggle-triggering behavior
Accessibility implications ¶
- a toggle-trigger element needs to become activatable/focusable/etc, and communicate in the a11y tree that it’s a checkbox/radio/etc
- we can infer what type of control it is by examining the properties of the toggle: if it’s part of a group, sticky, etc.
- if
toggle-visibility
is in use, we can also automatically infer all the tab-set ARIA roles
Interaction between scrolling, gestures, toggles? ¶
For the carousel, and other design patterns, it would be useful to have a two-way integration between toggles and scrolling/scroll-snapping, so that:
- Scrolling/snapping could trigger active state
- Changing active state could change scroll position
There are several other important interactions with related features that we need to keep in mind as well (some may require additional research):
- gestures (long-term, toggles should not be limited to tap/click/enter activation)
- non-user trigger events (could be useful to trigger toggles from e.g. an animation event)
- tab/accordion ‘panel set/section list’ element (aka ‘spicy sections’)
- popup attributes
- can open/close use toggles?
- can toggles work with ‘light dismiss’?
We are currently considering explicit activations to be a planned extension for level 2 of the spec – but it’s possible some of these interactions will need to be solved in level 1.
Considered Alternatives ¶
Previous CSS toggle states proposal ¶
ToDo:
Needs commentary
Declarative show-hide explainer ¶
ToDo:
Needs commentary
Stakeholder Feedback / Opposition ¶
No known opposition.
References & Acknowledgements ¶
This proposal was heavily influenced by the ‘Declarative Show/Hide’ work of Robert Flack, Nicole Sullivan, and others:
There is also a previous draft spec written by Tab Atkins Jr.:
ToDo:
- add
toggle()
function to get the value of a toggle? matchattr()
behavior. - look into using toggle for popup