Skip to content

Switch

“Our walls have no doors.”

“Every wall I’ve met said that,” Trace said.

Coldmere Systems, the sales meeting

A checkbox with role="switch". Screen readers then say “on” and “off” instead of “checked”, while the keyboard and FormData work as for an ordinary checkbox.

Choose Switch when the setting takes effect at once. If the choice should only apply when the form is submitted, Checkbox is right: a switch that does nothing until you press “Save” promises more than it delivers.

The switch promises that the change applies. So it also has to say when saving fails, and go back. fetch only rejects when the network fails, so a response with an error status has to be checked by you, otherwise a refusal from the server ends up in the branch that reports success. A redirect, as to the log-in page when the session has run out, is followed to a page that is ok, so it is checked too. In “Plain HTML”, React and kotlinx.html, another press is blocked while the save is in progress without disabled, which would take focus from the switch; in Datastar another press sends another @put, and the server decides. React uses the builder functions, Datastar its own attributes, and kotlinx.html the same script as “Plain HTML”.

<!-- autocomplete="off" stands on the form, since HTML does not allow it on
a checkbox. Without it Firefox keeps the state from before a reload. -->
<form autocomplete="off">
<div class="lt-switch-row">
<input class="lt-switch" type="checkbox" role="switch" id="auto-renew" />
<label class="lt-label" for="auto-renew">Auto-renew</label>
</div>
</form>
<p class="lt-help-text" id="auto-renew-status" role="status"></p>
<script type="module">
const toggle = document.getElementById("auto-renew")
const status = document.getElementById("auto-renew-status")
let saving = false
// Another press while saving should not flip the switch. `disabled`
// would block it the same way, but take focus from it.
toggle.addEventListener("click", (event) => {
if (saving) event.preventDefault()
})
toggle.addEventListener("change", async () => {
const on = toggle.checked
saving = true
try {
const response = await fetch("/api/subscription", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ autoRenew: on }),
})
// A redirect, as to the log-in page when the session has run out, is
// followed to a page that is ok, but nothing was saved.
if (!response.ok || response.redirected) throw new Error(response.statusText)
status.textContent = on ? "Auto-renew is on." : "Auto-renew is off. Your walls expire with the subscription."
} catch {
toggle.checked = !on
status.textContent = "Could not save. Try again."
} finally {
saving = false
}
})
</script>

role="status" makes screen readers announce the result without interrupting what the user is doing.

ClassWhat it is
.lt-switchThe switch itself
.lt-switch-rowThe row that holds the switch and the text side by side
VariableDefault
--lt-switch-widthvar(--lt-spacing-10)
--lt-switch-heightvar(--lt-spacing-6)

The knob inside is a gradient and not a pseudo-element, because Firefox does not render ::before and ::after on an <input>. It therefore follows the height of the switch by itself. See Customising.

The switch slides between off and on. If the user has asked for reduced motion in their system, it changes state without a transition instead.

What a screen reader is given when the recipe above is used, step by step. It is checked through AT-SPI, the interface Orca reads, in Chromium and Firefox, on every change, by check-accessibility-tree.ts, and every change the check lists as a mutation must make it fail.

  1. Focus #auto-renew: focus on the switch "Auto-renew", not checked.
  2. Press Space: focus on the switch "Auto-renew", not checked; "Could not save. Try again." announced once, polite.
  • The label should say what the switch controls, not which state it is in. “Lethal response”, not “Lethal response is on”.
  • Do not write “Off” and “On” as text next to it. The screen reader already says it, and the text contradicts itself when the user flips the switch.
  • The switch has no invalid state. If switching it on is required, for example to accept terms, that belongs in a Checkbox with required.
  • If the change needs confirmation or cannot be undone, use a button instead. A switch that opens a dialog is not a switch.