CSS Toggle States

Unofficial Proposal Draft,

This version:
http://tabatkins.github.io/specs/css-toggle-states/Overview.html
Issue Tracking:
Inline In Spec
GitHub Issues
Editor:
Tab Atkins Jr. (Google)
Suggest an Edit for this Spec:
GitHub Editor

Abstract

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

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

Status of this document

1. Introduction

This section is not normative.

Some user-interface languages define elements which can have "toggleable state", which can be modified by user interaction and selected using CSS Selectors. 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.

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.

This module generalizes this ability and allows it to be applied to any element via CSS. Elements can be declared to have toggleable state, with any number of states that can be toggled between. Multiple elements can share access to the same toggleable state, similar to HTML’s <input type=radio> element. This state can be manipulated by activating the element or other specified elements, or by other user interactions.

2. Creating a Toggleable Element: the toggle-states and toggle-initial property

Name: toggle-states
Value: none | <integer> [cycle | sticky]?
Initial: none
Applies to: all elements
Inherited: no
Percentages: n/a
Computed value: as specified
Canonical order: per grammar
Media: interactive
Animatable: no
Name: toggle-initial
Value: <integer>
Initial: 0
Applies to: all elements
Inherited: no
Percentages: n/a
Computed value: as specified
Canonical order: per grammar
Media: interactive
Animatable: no

The toggle-states property controls whether an element has toggleable state or not.

none
Indicates that the element does not have toggleable state.
<integer> [cycle | sticky]?
Indicates that the element is toggleable. The <integer> gives the number of states; it must be 2 or greater, or else the property is invalid. The following optional keyword defines the behavior when the element is already at its last state, and is toggled again: cycle defines that it should cycle back around to the first state, while sticky defines that it should stay at the last state.

If an element is toggleable, it has a toggle state, which is an integer from 0 to some maximum value, as defined by toggle-states. This state is incremented whenever the element is activated by the user, using the same defininition of "activated" as the :active pseudo-class, and can be selected by the :checked or :checked() pseudo-classes, as defined later in this specification.

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-states: 2;
}
li:checked {
  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.

The toggle-initial property sets the initial toggle state of the element. Its value must be a non-negative integer, or else the property is invalid. If the value is equal to or greater than the number of states defined by toggle-states, it computes to the greatest toggle state.

2.1. Linking Toggle States: the toggle-group property

Name: toggle-group
Value: none | <custom-ident>
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 toggleable element’s toggle state is independent; incrementing one has no effect an any other. The toggle-group property allows elements to link their toggle states together by declaring them to be part of a named toggle group, such that only one can have a non-zero toggle state at a time, similar to HTML’s <input type=radio> element. Each toggle group on a page is identified by a unique <custom-ident>.

none
The element is not in a toggle group. It’s toggle state is independent of any other elements'.
<custom-ident>
The element is in the toggle group named by the <custom-ident>. Any time an element in the given toggle group has its toggle state incremented, the toggle state of every other element in the same toggle group is set to 0.

The keyword none is excluded from this <custom-ident>.

For example, toggle-group can be used to control a tabbed display, so that only one panel is displayed at a time:
.tab {
  toggle-states: 2 sticky;
  toggle-group: "tabs";
  toggle-share: select(attr(for idref));
}
.panel {
  toggle-states: 2 sticky;
  toggle-group: "panels";
}
.tab:checked {
  /* styling for the active tab */
}
.panel:not(:checked) {
  display: none;
}

Clicking on any tab will increment its toggle state from 0 to 1 (and the sticky keyword will keep it at 1 if activated multiple times), while resetting the rest of the tabs' toggle states to 0. The same happens to the panels, using the toggle-share property defined in a later section. The active tab and panel are styled and shown differently than the other tabs and panels.

The name is global to the page. Should we have a way to specify more implicit groups? Maybe a parent keyword, or a way to scope names to a subtree?

2.2. Sharing Toggle Activations: the toggle-share property

Name: toggle-share
Value: none | <selector-list>
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, activating an element only increments its own toggle state. The toggle-share property allows an element to additionally increment the toggle state of another element.

none
Activating the element only increments its own toggle state (assuming the element is toggleable).
<selector-list>
Activating the element increments the toggle state of both itself and all the elements matched by the given selector (assuming the elements are toggleable).

If the set of elements sharing the activation includes elements that share a toggle group, only the last such element (in document order) per group has its toggle state incremented.

The preceding section for toggle-group contained an example showing off toggle-share. When the user activates a tab, the activation is shared with the associated panel.

Note: This functionality is similar in nature to HTML’s <label> element, but not identical. toggle-share causes the activation to be shared, rather than transferred like <label> does. Additionally, this sharing only affects the toggle state, while <label more strongly transfers the concept of "activating", affecting things such as click events and the :hover pseudo-class.

3. Selecting Elements Based on Toggle State: the :checked and :checked() pseudo-class

The :checked pseudo-class, defined in [SELECTORS-4], has its definition expanded to additionally match any element whose toggle state is non-zero.

The :checked(<integer>) pseudo-class selects any elements whose toggle state has the value given by its argument.

3.1. Problems with Combining :checked and toggle-* Properties

Naively combining :checked and the toggle-* properties causes circularity issues.

For example, in the following code, the element starts in a state that matches :checked, but the :checked selector makes the element no longer toggleable, so it no longer matches :checked.
#foo {
  toggle-states: 2;
  toggle-initial: 1;
}
#foo:checked {
  toggle-states: none;
}

To avoid this, the toggle-states and toggle-initial are defined as selector-affecting properties, and the :checked and :checked() pseudo-classes are defined as property-affected selectors. In any style rule whose selector includes a property-affected selector, any selector-affecting properties are invalid.

I’m not sure this is sufficient. For example, you could set the property in an animation, which is triggered by an affected selector. Is there a better way to define this so that it’s reliable?

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.

Requirements for Responsible Implementation of CSS

The following sections define several conformance requirements for implementing CSS responsibly, in a way that promotes interoperability in the present and future.

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 property 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.

Implementations of CR-level Features

Once a specification reaches the Candidate Recommendation stage, implementers should release an unprefixed implementation of any CR-level feature they can demonstrate to be correctly implemented according to spec, and should avoid exposing a prefixed variant of that feature.

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-VALUES-3]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 3. 14 August 2018. CR. URL: https://www.w3.org/TR/css-values-3/
[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. 14 August 2018. WD. URL: https://www.w3.org/TR/css-values-4/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[SELECTORS-4]
Elika Etemad; Tab Atkins Jr.. Selectors Level 4. 2 February 2018. WD. URL: https://www.w3.org/TR/selectors-4/

Property Index

Name Value Initial Applies to Inh. %ages Ani­mat­able Canonical order Com­puted value Media
toggle-group none | <custom-ident> none all elements no n/a no per grammar as specified interactive
toggle-initial <integer> 0 all elements no n/a no per grammar as specified interactive
toggle-share none | <selector-list> none all elements no n/a no per grammar as specified interactive
toggle-states none | <integer> [cycle | sticky]? none all elements no n/a no per grammar as specified interactive

Issues Index

The name is global to the page. Should we have a way to specify more implicit groups? Maybe a parent keyword, or a way to scope names to a subtree?
I’m not sure this is sufficient. For example, you could set the property in an animation, which is triggered by an affected selector. Is there a better way to define this so that it’s reliable?