HTML is where mature JavaScript patterns graduate to, removing the need for scripting for common, well-established user interactions. Having a button switch some state on click is a common pattern that needs to move into a declarative HTML-only setup.
Button-type buttons (<button type="button"></button>
) have no inherent behavior. Currently, they require scripting to do anything. A common behavior added is to switch some state:
- Menu button toggles the state of a menu between "open" and "closed"
- Toggle button switches a site setting, e.g. between "dark mode," "light mode," and "match OS"/"auto"
- Tab/accordion button switches the state of the tabset or accordion to one particular section (alternatively, it sets the state for the designated section to "active/open/visible" and the states for all other sections to "inactive/closed/hidden")
Statefulness has long been a part of web pages, with browsers doing extra work to try to restore the state of a page when navigating back to it. Consider filled out form fields and scroll position, both of which the browser attempts to restore when possible. CSS enables reading and reacting to statefulness with pseudo-selectors like :checked
, :hover
, :invalid
, :disabled
, etc. Custom web components have a dedicated API for introducing, modifying, and reading state: CustomStateSet
and :state()
.
This proposal attempts to generalize the markup for creating, changing, and reading state without requiring scripting. The CustomStateSet
API should be made available on stateful elements so that states can be managed via Javascript as well.
Consider checkboxes, where the attribute checked
conveys only the initial state and can't be changed to be unchecked (it must be removed to uncheck), and needs several pseudo-selectors to read state :indeterminate
, :checked
, and :unchecked
. Additionally, if the DOM property for the input is assigned anything (e.b. el.checked = false;
), attribute reflection is broken and adding or removing the checked
attribute no longer has an effect. Alternatively, this could be achieved with
<style>
button[role="switch"] {
/* basic styles */
&:state(unchecked) {
/* unchecked styles */
}
&:state(checked) {
/* checked styles */
}
}
</style>
<button
type="button"
states="checked unchecked"
role="switch"
aria-checked="fromstate(checked as true, unchecked as false)">
</button>
This proposal allows for many-to-many mappings between buttons and stateful element targets. Each button can set (the same) state for multiple stateful elements by listing their IDs in the statefor
attribute. Each stateful element can have multiple buttons targeting it, each with its own set of states to transition the element into.
<style>
#many-to-one {
&:state(red) { color: red; }
&:state(blue) { color: blue; }
&:state(green) { color: green; }
&:state(yellow) { color: yelow; }
}
</style>
<div id="many-to-one">Lots of buttons can switch my state</div>
<button type="button" statefor="many-to-one" states="blue red">Blue or red</button>
<button type="button" statefor="many-to-one" states="green yellow">Green or yellow</button>
<button type="button" statefor="many-to-one" states="blue green">Blue or green</button>
<style>
#one, #two, #three {
&:state(open) { height: auto; }
&:state(closed) { height: 0; }
}
</style>
<div id="one">One</div>
<div id="two">Two</div>
<div id="three">Three</div>
<button type="button" statefor="one two three" states="open close">Open or close all</button>
<style>
#one {
/* defaults to closed */
> .content { height: 0; }
&:state(one) > .content { height: auto; }
}
#two {
/* defaults to closed */
> .content { height: 0; }
&:state(two) > .content { height: auto; }
}
#three {
/* defaults to closed */
> .content { height: 0; }
&:state(three) > .content { height: auto; }
}
</style>
<div id="one">
<span>One<span><button type="button" statefor="one two three" states="one">See more</button>
<div class="content">
Content that is hidden when closed
<button type="button" statefor="one" states="close">See less</button>
</div>
</div>
<div id="two">
<span>Two<span><button type="button" statefor="one two three" states="two">See more</button>
<div class="content">
Content that is hidden when closed
<button type="button" statefor="two" states="close">See less</button>
</div>
</div>
<div id="three">
<span>Three<span><button type="button" statefor="one two three" states="three">See more</button>
<div class="content">
Content that is hidden when closed
<button type="button" statefor="three" states="close">See less</button>
</div>
</div>
<button type="button" statefor="one two three" states="close">Close all</button>
statefor
: an id or space-separated list of ids.
- Only valid (for designating stateful element targets) on
button
elements withtype="button"
- If not present, any
states
are applied to the button itself
states
: a space-separated list of possible states.
- When applied to a button, it indicates which states it cycles through. If the stateful element is not currently in one of the states listed, clicking the button will set the element to the first state. If the stateful element is in one of the states listed, it will transistion to the next in the list, or the first if the element is in the last state listed.
- When applied to a stateful element (which could be the button itself), it creates an explicit sequence of states that can be used to inform what the "next" state is, and it sets the initial state to the first in the sequence. This explicit list does not restrict possible states. The element can be put into a state not listed (the "next" state would then be the first state listed). For example, in the following setup, the
div
is in the initial state of3
because that is the first state listed and when the "Next even" button is clicked, it will transition to4
rather than2
(which would be the case if thediv
didn't have an explicit state list). When the "Zero" button is clicked, thediv
is transistioned to the0
state, and a subsequent click of "Next even" would transistion it to2
because0
isn't in the explicit states list and2
is the first state in the "Next even" button's state list.
<div id="count" states="3 4 5 6 7 8 1 2"></div>
<button type="button" statefor="count" states="1 3 5 7">Next odd</button>
<button type="button" statefor="count" states="2 4 6 8">Next even</button>
<button type="button" statefor="count" states="0">Zero</button>
fromstate()
: a function for reading state and applying it to a property in HTML. Can have zero or more arguments.
- Zero arguments applies the current state as a string to the property, e.g.
<input type="fromstate()"><button type="button" states="text number date">change input type</button>
- Arguments are reassignments of states to strings, in the format
<state> as <string>
. Any states not explicitly listed in the arguments return themselves as strings, like they do when no arguments are passed. The order of the arguments does not affect the state switching sequence.
<input id="Img" type="file" accept="image/*" capture="fromstate(forward as environment, selfie as user)">
<button type="button" states="selfie forward" statefor="Img">switch camera</button>
statechange
event: An event dispatched on the stateful element when its state changes. Bubbles and is not cancelable. Dispatches after the click
event, unless that event is canceled. (If the click
event is canceled, the stateful element is not transistioned to a new state.) Has the following properties:
relatedTarget
: thebutton
element that triggered the state change ornull
if it was triggered via scriptprevState
: the element's previous state ornull
if the element didn't have one.state
: the element's current state
onstatechange
attribute: An inline handler for statechange
events. This could be used to simulate a simplified version of the invoker commands proposal:
<dialog id="info" onstatechange="this.states.has(open) ? this.showModal() : this.close()">
<p>some content</p>
<button type="button" statefor="info" states="close">Close</button>
</dialog>
<button type="button" statefor="info" states="open">Open</button>
It will be up to the browser to expose states to the Accessibility Object Model (AOM) and assistive tech (e.g. screen readers) to incorporate a stateful element's state. The proposed statefor
attribute on buttons could be used in place of aria-controls
. A set of "well-known" states could be used in place of aria-checked/current/disabled/busy/hidden/invalid/pressed/selected
and any other states currently declared via ARIA attributes.
Where ARIA attributes currently apply, authors can use the fromstate()
value function to add dynamic values that change with the element's state.
- Should other elements be allowed to be state-setting elements?
<button>
(no type, defaults to"submit"
)<button type="submit|reset">
<input type="button|submit|reset|radio|checkbox">
- Should
fromstate()
allow for a default mapping, so unknown states will get a certain string? LikefromState(someState as someString, @else as defaultString)
- Should
fromstate()
allow for removing boolean attributes? Likeopen="fromstate(closed as remove())"
fordialog
elements. - Should buttons targeting multiple stateful elements be allowed to set different states per element? Or, if we allow for multiple states to co-occur on a single element, can a single button set multiple states on the same element?
- Since the
CustomStateSet
accounts for multiple co-occuring states, should multiple states be allowed? Something like
<style>
#X {
&:state(color:red) { color: red; }
&:state(color:blue) { color: blue; }
&:state(color:green) { color: green; }
&:state(size:small) { width: 5em; }
&:state(size:medium) { width: 10em; }
&:state(size:large) { width: 15em; }
/* combo state */
&:state(color:red):state(size:large) {
border: 3px dashed orange;
}
}
</style>
<div id="X">target</div>
<button type="button" statefor="X" states="color: red green blue">set color</button>
<button type="button" statefor="X" states="size: small medium large">set size</button>
- Reading state from arbitrary elements in HTML, e.g.
prop="fromstate(#some-id, state1, state2)"
. (Reading state from other elements is possible in CSS with the:has()
structure selector.) - Setting sub-states. An element may be in a single state (in this proposal). For example, consider an element that can have the states
car
, andtruck
, as well asblue
, andred
. Sub-states would allow some dynamic permutation of this (e.g.setstate="car"
preserving the color state, or:state(red)
matching bothtruck
andcar
states). This proposal requires that the buttons setting the states provide the whole state (blue-car
,blue-truck
,red-car
, andred-truck
) and the CSS state selector reads the same whole state. - Setting infinite state. For example, a counter with no cap.
This proposal is abandoned
https://tabatkins.github.io/css-toggle/
This proposal constructed the entirety of the state declaration and switching behavior in CSS only. The HTML would have no knowledge of the states or the connection between stateful elements and state-setting buttons (called triggers in this proposal). It defaults to allowing an infinite list of states (1, 2, 3, ...) but allows a list of enums too. It allows for transistioning between states in both directions, forward and backward. It allows for different cycling behavior, sticking to the last state, or starting over at the first state.
https://open-ui.org/components/invokers.explainer/
This proposal is heavily reliant on JavaScript. It introduces two new attributes for button
elements, command
and commandfor
, and their DOM API counterparts, command
and commandForElement
respectively. It enforces a 1-to-1 or a many-to-1 mapping between buttons and targets (multiple buttons can have the same target, but a single button cannot have multiple targets). It relies on built-in methods present on the target elements, e.g. showModal()
on a dialog
element. This proposal calls for the dispatching of a cancelable command
event on the target element. If JavaScript is disabled, these attributes will have no effect.