# Switch

> A switch for a setting that takes effect immediately. CSS component with two classes and no JavaScript.

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](/components/checkbox.md) is right: a switch that does nothing until you press “Save” promises more than it delivers.

## How to use it

**Plain HTML**

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/switch/switch.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/label/label.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/help-text/help-text.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->
```

```html
<div class="lt-switch-row">
<input class="lt-switch" type="checkbox" role="switch" id="email" checked />
<label class="lt-label" for="email">Notify me by email</label>
</div>


<div class="lt-switch-row">
<input class="lt-switch" type="checkbox" role="switch" id="sms" />
<label class="lt-label" for="sms">Notify me by text message</label>
</div>


<div class="lt-switch-row">
<input class="lt-switch" type="checkbox" role="switch" id="post" disabled />
<label class="lt-label" for="post">Notify me by post</label>
</div>
```

**TypeScript**

```ts
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/switch.css"
import "@lo-tek/interfaces/label.css"
import "@lo-tek/interfaces/help-text.css"
```

`lt` is used in two ways, and which one depends on whether you create the element or find it.

**If you create the markup,** you spread the attributes in. That is the route in React, Astro and anything else that renders:

```tsx
import { lt } from "@lo-tek/interfaces/react"


<div className={lt.switch.row}>
  <input {...lt.switch()} id="notifications" />
  <label {...lt.label()} htmlFor="notifications">Notify me by email</label>
</div>
```

The import goes to `/react` here, since the block is JSX. From there the keys are called `className` and `htmlFor`. In Astro and anything else without React you import `@lo-tek/interfaces` and get `class` and `for`.

**If the element is already there,** you set them on it. That is the route when you enhance HTML a server or a template has sent:

```ts
import { lt } from "@lo-tek/interfaces"


const toggle = document.querySelector<HTMLInputElement>("#notifications")


// The server sent a checkbox. Now it is a switch.
lt.setAttributes(toggle, lt.switch())


// A setting the user is not allowed to change.
lt.setAttributes(toggle, lt.switch({ disabled: true }))
```

The function sets both `type` and `role`, which are the two things that must go together for the switch to be announced correctly. `lt.switch.row` gives the class for the row around it.

Do not disable the switch to prevent another press while a save is in progress. `disabled` takes focus away from a switch that has it, focus does not come back when it is enabled again, and another press of the space bar does nothing. See [Saving immediately](#saving-immediately) for how to block another press without it.

If you import it directly instead of through `lt`, it is called `switchControl`, since `switch` is a reserved word in JavaScript:

```ts
import { switchControl } from "@lo-tek/interfaces"
```

## Saving immediately

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

**Plain HTML**

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

**React**

```tsx
import { useState } from "react"
import { lt } from "@lo-tek/interfaces/react"


export function NotificationSwitch({ initial }: { initial: boolean }) {
  const [on, setOn] = useState(initial)
  const [saving, setSaving] = useState(false)
  const [status, setStatus] = useState("")


  async function changed(value: boolean) {
    // While a save is in progress, the switch stays where the state says.
    // `disabled` would block it the same way, but take focus from it.
    if (saving) return


    // Flip at once, so the switch does not lag behind the finger.
    setOn(value)
    setSaving(true)
    setStatus("")


    try {
      const response = await fetch("/api/notifications", {
        method: "PUT",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ email: value }),
      })
      if (!response.ok) throw new Error(response.statusText)
      setStatus(value ? "Notifications are on." : "Notifications are off.")
    } catch {
      // Back to where it was. The switch promised something it did not keep.
      setOn(!value)
      setStatus("Could not save. Try again.")
    } finally {
      setSaving(false)
    }
  }


  return (
    <>
      <div className={lt.switch.row}>
        <input
          {...lt.switch()}
          id="notifications"
          checked={on}
          onChange={(e) => changed(e.currentTarget.checked)}
        />
        <label {...lt.label()} htmlFor="notifications">Notify me by email</label>
      </div>
      <p {...lt.helpText()} role="status">{status}</p>
    </>
  )
}
```

`checked` and `onChange` together make the field controlled, and that is exactly what makes the rollback possible: React draws the switch from your state, not from what the user clicked. It is also what keeps the switch in place during the save: when `changed` does not change the state, React draws it back.

**Datastar**

```html
<!-- Datastar itself. Without it the `data-` attributes below do nothing. -->
<script type="module"
        src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js"></script>


<div class="lt-switch-row"
     data-signals="{notifications: false, status: ''}">
  <input class="lt-switch" type="checkbox" role="switch" id="notifications"
         data-bind:notifications
         data-on:change="@put('/api/notifications')" />
  <label class="lt-label" for="notifications">Notify me by email</label>
</div>
<p class="lt-help-text" role="status" data-text="$status"></p>
```

The server decides the outcome here. `@put` sends the signals, and the response sets `status` and, when the save failed, `notifications` back to where it was. The rollback is therefore in the server and not in the page, as for all other state the server decides. Another press while the save is in progress sends another `@put`, and the server decides that one too; a `disabled` guard in the page would take focus from the switch.

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

## Classes you can style

| Class | What it is |
| - | - |
| `.lt-switch` | The switch itself |
| `.lt-switch-row` | The row that holds the switch and the text side by side |

| Variable | Default |
| - | - |
| `--lt-switch-width` | `var(--lt-spacing-10)` |
| `--lt-switch-height` | `var(--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](/customising.md).

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.

## Accessibility

- 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](/components/checkbox.md) 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.
