Skip to content

Switch

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. In “Plain HTML”, Astro and React, 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. The markup is the same in “Plain HTML” and Astro; React uses the builder functions, and Datastar its own attributes. Astro uses the same script as “Plain HTML”.

<div class="lt-switch-row">
<input class="lt-switch" type="checkbox" role="switch" id="notifications" />
<label class="lt-label" for="notifications">Notify me by email</label>
</div>
<p class="lt-help-text" id="notifications-status" role="status"></p>
<script type="module">
const toggle = document.getElementById("notifications")
const status = document.getElementById("notifications-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/notifications", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: on }),
})
if (!response.ok) throw new Error(response.statusText)
status.textContent = on ? "Notifications are on." : "Notifications are off."
} 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.

  • The label should say what the switch controls, not which state it is in. “Notify me by email”, not “Email notifications 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.