CSS Toggle States

A Collection of Interesting Ideas, 9 July 2013

This version:
http://www.w3.org/TR/2013/DREAM-css-toggle-states-1-20130709/
Editor's Draft:
http://tabatkins.github.io/specs/css-toggle-states/Overview.html
Feedback:
www-style@w3.org with subject line “[css-toggle-states] … message topic …” (archives)
Test Suite
None Yet
Editors:
Tab Atkins Jr. (Google)

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.

Status of this document

This is a really unofficial draft. It's not meant to capture any consensus, beyond my own personal feelings about what sounds interesting. It is provided for discussion only and may change at any moment, and should not be taken as "official" or even "unofficial, but planned". Its publication here does not imply endorsement of its contents by W3C. Don't cite this document other than as a collection of interesting ideas.

Table of contents

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
Media:interactive
Computed value:as specified
Animatable:no
Name:toggle-initial
Value:<integer>
Initial:0
Applies to:all elements
Inherited:no
Percentages:n/a
Media:interactive
Computed value:as specified
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 | <string>
Initial:none
Applies to:all elements
Inherited:no
Percentages:n/a
Media:interactive
Computed value:as specified
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.

none
The element is not in a toggle group. It's toggle state is independent of any other elements'.
<string>
The element is in the toggle group named by the <string>. 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.
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>
Initial:none
Applies to:all elements
Inherited:no
Percentages:n/a
Media:interactive
Computed value:as specified
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>
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 and :checked() pseudo-classes select elements based on their toggle state.

The :checked pseudo-class selects any elements 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?

References§

Normative References§

Informative References§

Index§

Property index§

NameValueInitialApplies ToInh.%agesMediaComputed valueApplies toAnimatable
toggle-statesnone | <integer> [cycle | sticky]?nonenon/ainteractiveas specifiedall elementsno
toggle-groupnone | <string>nonenon/ainteractiveas specifiedall elementsno
toggle-initial<integer>0non/ainteractiveas specifiedall elementsno
toggle-sharenone | <selector>nonenon/ainteractiveas specifiedall elementsno