Tab Completion

I'm Tab Atkins Jr, and I wear many hats. I work for Google on the Chrome browser as a Web Standards Hacker. I'm also a member of the CSS Working Group, and am either a member or contributor to several other working groups in the W3C. You can contact me here.
Listing of All Posts

Toggling State in CSS

Last updated:

This is a rough proposal for porting toggle states (the things that checkboxes, radio buttons, and <details> have) into pure CSS.

The Use-Case

Virtually every time I produce a page with non-trivial user interaction, I end up wanting to have something that toggles between "clicked" and "not clicked" in some way.

For example, I have a small recipe app for personal use. You can click on ingredients as you prepare them, and they turn light gray and crossed out to help guide your eyes away from them, so you can focus just on the remaining ingredients that you need to deal with.

For another example, any time I make something using tabs, I want to show/hide some panels based on clicking on some tabs, and style the "active" tab differently as well.

For a third example, I sometimes want a popup dialog to show up when I click some button, which closes itself when you click the page-covering background.

There's just tons of little examples you produce all the time, which are basically just the functionality of radio buttons and checkboxes.

These could be done with JS, but that's fiddly and annoying. I've got to wait for the DOM to load, attach click handlers (and touch handlers!), and then add/remove classes.

Instead, I nearly always end up using hidden <input> elements. For example:

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

Here, it's just a little bit of style, plus an annoying bit of extra markup, and everything just works. No JS, and it's very dependable. This example can be generalized pretty obviously to address the other use-cases I provided.

Ideally, that annoying bit of extra markup could be eliminated, though. It seems so simple!

Proposal for toggle-* properties

I propose the following four properties which, taken together, implement a slight superset of what you can do with checkboxes and radio buttons.

toggle-states:  none | <integer> sticky? | infinity  (initial: none)
toggle-group:   none | <ident>  (initial: none)
toggle-share:   none | <selector>#  (initial: none)
toggle-initial: <integer>  (initial: 1)

toggle-states is the basic function that turns toggle functionality on/off. It defaults to none, which just means the element doesn't toggle at all. Alternately, you can set it to an 2 or greater, which defines the number of states it toggles between. The first state is, by convention, the "off" state, and all further states are different flavors of "on". Normally, it'll just cycle between all the states in order; if you reach the end and click again, it'll return to the first state. If the sticky keyword is provided, it'll skip the "off" state when it's cycling back around (this is needed for radio buttons, which stay clicked once you click them once). Obviously, infinity doesn't need stickyness, since there's no end. (The actual number of states supported is impl defined, but is required to be above some minimum number, like 256 or something.)

toggle-group implements the radio-button functionality. If you click an element with toggle-group set to a non-none value, all other elements in the page with the same toggle-group value will automatically be set to their first state (that is, turned "off").

toggle-share implements the <label> functionality. When you click an element with toggle-share, it acts as if you'd clicked all the elements it points to as well. The <selector> type is probably represented by the select() function, currently being defined in SVG2. This is implemented as "sharing" instead of "redirecting" like <label> does for two reasons:

  1. <label> has to deal with actual clicking, dispatching events and the like, and it's just simpler if there's still only a single click event originating from a user's action. CSS toggling doesn't have the same problem.
  2. I've often found, when using <label>s for tabbed interfaces, that I want to style both the active label and the active panel. Right now I need to do that by pushing the hidden <input> further up the DOM, then using descendant selectors to actually style the two elements. It's much easier if you just actually toggle both of them.

toggle-initial just sets the initial state of the toggle counter, like what the checked attribute does on <input>, or open on <details>. It must be set to an integer 1 or greater.

These all rely on the concept of a "toggle counter", which every toggleable element has. Toggle counters start at the toggle-initial value whenever elements become toggleable, and increment whenever the user clicks the element (or does similar behaviors that mean the same thing - whatever would activate a checkbox). This means, for example, that removing an element from the document temporarily and then reinserting it will reset the toggle counter, since for a little while the element didn't match the selectors that make it toggleable.

A toggleable element should probably automatically be focusable, as well.

Selecting toggled elements

Now we come to the part that's actually difficult (where difficult means "impossible, unless we bite a bullet we usually try to avoid"): styling an element differently based on its toggle state.

The obvious way would be to just enhance the :checked pseudo-class: :checked would match any element which is toggleable and in a state > 1. We would also want to allow a new parametrized version of :checked, so that :checked(n) matches elements in state n only.

This immediately runs into a cyclic dependency problem, as illustrated by this example:

.foo {
  toggle-states: 2;
  toggle-initial: 2;
}
.foo:checked {
  toggle-states: none;
}

The above code says that while an element matches :checked, it's not toggleable (and thus doesn't match :checked). This is, to understate things, a problem.

(We already have some things that look like they trigger the same problem, but don't really, such as the :dir() selector and the direction property. We avoid problems here by saying that :dir() only pays attention to the direction determined from the document language, and ignores the direction property entirely.)

It's tempting to just bite this bullet and say that you can't use the toggle-* properties in a rule when :checked appears in the rule's selector.

This works exactly once; if we ever introduce a second thing with similar problems, you suddenly need full-blown cycle detection, as Selector A can set Property B, which triggers Selector B to set Property A, which causes Selector A to no longer match.

I had brief thoughts that we could get around this by not using selectors, and instead introducing a function that takes multiple values, and returns one corresponding to the toggle state (for example, I'd do my original example with color: toggle(black, silver); text-decoration: toggle(none, line-through);). This is more convenient in some cases, less convenient in others, but most importantly, it's functionally identical to using a selector, and so doesn't help us at all.

I have no idea how to solve this. As far as I can tell, it can't be solved. We must either rely on the document language somehow (like :dir() does), or else just bite the bullet and somehow do the "sorry, can't use that property in this block" thing.

I'm thinking that the latter might be possible if we generalize ahead of time, and state that all properties which affect selector state can't be set in any block that uses one of the affected selectors, even if the property in question isn't relevant to the selector in question. This cuts out some potentially useful interactions, but it's safe and inexpensive.

If anyone has any better suggestions, speak up!

Some Examples

The example code I gave above would be vastly simplified:

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

Implementing a tabbed display, where clicking on a tab shows its corresponding panel and hides the rest, would similarly become pretty trivial:

.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;
}

Thoughts?

If you've got any thoughts on this proposal, feel free to share them in the comments!

(a limited set of Markdown is supported)

#1 - Martin Heidegger:

It seems to me that your approach would not help you a lot with your initial example:

<label><input type=checkbox><span>1 cup blueberries</span> </label>
input:checked + span {
   color: red
}

if you click the input then the input will change from "not-checked" to "checked" but the span beside it will stay "not-checked" thus any toggle would stay inactive. I mean the selector doesn't define the toggle, doesn't it:

 div:checked input:checked+span {
    /* Two times checked? */
 }

Aside from that I recently tinkered with a js solution to the same problem where I essentially changed the class of labels with a for="" field.

(peudocode) <input id="a" onchange="$('label[for=a]').toggleClass("checked", this.checked)"> <label for="a">hi<label>

that allowed me to write label.checked { color: green; }

Which makes me think that "toggles" would be nice if we had n-states also for "referenced" fields (for="").

 <input id="a" type="checkbox"><label for="a">Hi</label>
 label {
     color: toggle(red, green)
 }

but then you could write

input:checked {}

but not

label:checked { }

which would be irritating, also that checked is a boolean in the html spec, so: making it a n-selectable would be weird. How about

input:state(1) {
}

and have same added as html property? Together with a proper support of label[for]?

label:state(1) {
}

yours Martin.

Reply?

(a limited set of Markdown is supported)

Re #1: No, if we had toggle-*, my example would instead be much simpler:

<ul class=ingredients>
  <li>one banana
  <li>one cup blueberries
  ...
</ul>
<style>
li { toggle-states: 2; }
li:checked {
  color: silver;
  text-decoration: line-through;
}
</style>

I had assumed this transformation was obvious, but if it's not, I should add this example into the post proper.

Regarding your question about labels, that's supported. To avoid confusion with built-in things, let's assume that you have tabs which, when clicked, toggle their corresponding panel. Just do:

.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;
}

Done! You can now click on the tab, and both it and its associated panel become :checked.

Reply?

(a limited set of Markdown is supported)

#3 - Martin Heidegger:

Re #2: Thank you for clarification. I start to like/love the idea. However, some thoughts:

1) toggle-share is a bit tricky in its definition. Technically we have two things in the background (if i understand it right)

a) A set of numbers { "tabs": 2 } that contains the current state b) A set of nodes { "tabs": { "1": { "checked":[".tab#tab1",. ".panel#panel1"], "unchecked":[".tab#tab2", ".panel#panel2"], "2": [...] } to see which tabs should be checked and which unchecked on state change (obviously the logic is a little more complex). With "toggle-group" we can adress which individuals trigger the toggle and get updated. But we do not define which element triggers which: choosing them automatically might be too restrictive in case you have two buttons that trigger the same toggle or with different order?! Side-note: I couldn't find the SVG select that you mentioned in the specs (google didn't help).

2) toggle-initial should be set in html, shouldn't it? I mean there is a reason why "checked" can be defined in html: so the server guy can send a file that has delivers the current application state (not what I change during runtime). I understand that it might be set by <ul style="toggle-initial=5"> but it doesn't look elegant. Btw.: This makes it confusing again to me: where would I set the initial state exactly? How would the css look like?

3) Did you add the "toggle-states" property in .panel just for show? Shouldn't it be clear that it has two states, because its shared with "tabs"?

I admit most of my questions might seem odd, mostly related to the late hour. But here is what I am thinking: <input type="checkbox" value="a"> needs the definition of a to know which checkbox should be selected. Perhaps this spec needs something like that too

<ul toggle-init="b"> <li toggle="a"> <li toggle="b"> </ul> <div class="panel" toggle="a">

then it can be defined as:

ul, ul.li, .panel { toggle-group: mygroup; }

.panel:not(:checked) { display: none; }

Reply?

(a limited set of Markdown is supported)

This looks pretty convenient, but does it belong in CSS? It has nothing to do with style/layout per se.

I think it's somewhat reminiscent of the 'behavior' property introduced by Microsoft: very convenient, but sorta out-of-place.

Reply?

(a limited set of Markdown is supported)

Re #4: It has everything to do with style/layout, it just interacts with user behavior as well. After all, this is literally just an enhancement of the existing :checked pseudoclass. Are you trying to argue that :checked doesn't belong in CSS?

Reply?

(a limited set of Markdown is supported)

Re #5: No, I think :checked does belong in CSS, but toggle-group, toggle-share and co do not. These properties describe how these elements behave, not how they look. You're even introducing semantics with toggle-share (ie. this and this are related).

It's much more than "just an enhancement" of :checked, as it's a very different story for selectors: no matter how involved a selector is with attributes/state/semantics, it still fits in CSS because it acts as a filter for elements, and the outcome is ultimately used for styling. But when you introduce CSS properties that affect attributes/state/semantics, you're changing the role of CSS and causing overlap with HTML.

Reply?

(a limited set of Markdown is supported)

Re #6:

They only "describe how elements behave" insofar as they expand the set of elements that can match :checked.

Looking at CSS through too narrow of a lens is artificially limiting. CSS is a convenient notation for applying optional details to your page. This has traditionally been used for "styling", but early on it also proved useful for layout (and we're obviously expanding that), and even some minor behavioral tweaks (ones that were simple enough to express in a declarative property).

There's no reason to artificially segment out "behavior" into something that only JS can do. Use the best tool for each job.

I'll note, though, that you're claiming overlap with HTML. That's interesting. This has practically no overlap with HTML as far as I can tell. It's inspired by the functionality of some HTML form elements, but only because those form elements have become so commonly abused for non-semantic styling purposes. The whole point of this proposal is to fix that, so that I can continue to use checkboxes solely for their actual purpose in a <form>, and stop misusing them for other purposes.

Reply?

(a limited set of Markdown is supported)

Re #7:

Still, it would be nice to have the functionality in HTML as well. I'd have no problems with form elements like checkboxes supporting several states ... and it would eliminate the cyclic dependency problem, if toggle-states, toggle-group etc would be defined in HTML, wouldn't it?

Is there a specific reason for making 1 representing the off state? I figure 0 would somehow be more natural ... so basically every element would have the 0/off state as a default. So maybe it would make sense to extend the meaning to generally represent states? For a checkbox it'd still be unchecked/checked, for an input element the off state could mean its unchanged and still set to the initial default value. But also for e.g. a menu with nested lists (treemenu, the states defining which branches are expanded), a paragraph containing text that can be expanded and collapsed etc .... or does something like this already exist?

Reply?

(a limited set of Markdown is supported)

This whole CSS/JS overlap is a bit tricky. The :checked selector doesn't add the toggle-functionality, it just reacts to it. The toggle-* functions do both. I think it's a great idea to be able to react to toggle-states but where to set the dependencies if not in CSS? If we would set them in JS, this whole thing would be pointless, since we wanted a no-js solution. Maybe new HTML-attributes, but then tzere would be a new dependenciey between markup and style AND interaction. As I said, it is tricky. Maybe a smaller approach would work, like extending :checked to more elements than just inputs.

I'd really like to see something like this someday.

Reply?

(a limited set of Markdown is supported)

Would it be possible to retain the current toggle state, even if the number of states is reduced to none? Simply preventing the user from toggling the state further. This may also be useful if the toggling states are ever returned.

Reply?

(a limited set of Markdown is supported)

Why you won't to play with :focus/:active and attribute tabindex? Example http://www.cssplay.co.uk/menus/cssplay-click-drop-fly-ipad-v2.html

Reply?

(a limited set of Markdown is supported)