---
title: Switch
description: A checkbox that admits it: a real input with role=switch, and a track :checked drives.
source: https://adamjurek.com/components/switch
---

## Home


Switch is a checkbox that admits what it is: a real input carrying
`role="switch"`, drawn as a track and thumb on its label. Reach for it for a
setting that takes effect the instant it is toggled, not one that waits for a
form to be submitted.

<!-- ::start:showcase demo="switch" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

`role="switch"` without a managed `aria-checked` is a promise half kept, so
the input underneath stays a genuine, native checkbox rather than a styled
div faking the semantics - announced honestly, with the track and thumb
driven by `:checked` rather than by React state.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/switch.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/switch.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Forms |
| Files | `switch.tsx` |
| Dependencies | None |
| Tags | form, toggle, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Switch } from "@sushindustries/ui";

export function Example() {
	return <Switch label="Smooth scrolling" defaultChecked />;
}
```

## What you should see

A pill-shaped track with a round thumb, the label text to its right, both
inside one clickable label. Checked, the track fills with the accent color
and the thumb slides to the right edge. Click anywhere on the row, or tab to
it and press Space - it is a real checkbox underneath, so both already work.

## If nothing happens

Passing `checked` without `onChange` makes it a read-only input - the thumb
never moves, clicks do nothing, and React logs a warning in the console.
Use `defaultChecked` for an uncontrolled switch, or pair `checked` with
`onChange` for a controlled one.


## Guides


## Composing it

`Switch` is already a complete `<label>` wrapping the input and its text -
`label` is required for that reason, there is no bare unlabelled input to
reach for. Do not nest it inside a `Field`; `Field` renders its own
`<label>` around whatever it is given, and a `Switch` inside one gets
labelled twice.

## Motion and reduced motion

The thumb's slide and the track's colour change are both CSS transitions,
and both are removed entirely under `prefers-reduced-motion: reduce` - the
checked state still changes instantly, just without the 180ms of travel.

## When not to use it

For a choice that only takes effect once a form is submitted - agreeing to
terms, opting into a newsletter - use `Checkbox` instead. `role="switch"`
tells assistive technology the change is immediate, the way a setting in an
app is; a checkbox in a form implies "mark this, then submit," which is a
different promise to make to someone using a screen reader.


## API


<!-- generated:api -->

## Props

Accepts every prop of `Omit<InputHTMLAttributes<HTMLInputElement>, "type">`, plus:

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `label` | `string` | - |  |

<!-- /generated:api -->

## Notes

The input underneath is a real `<input type="checkbox">`, so every native
checkbox prop applies as-is - `defaultChecked`, `checked`, `onChange`,
`disabled`, `required`. `type` is not in the prop list because it is fixed;
there is no way to make this render anything other than `role="switch"` on
a checkbox.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="switch" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { useState } from "react";
import { Switch } from "@sushindustries/ui";

export function ScrollPreferences() {
	const [smooth, setSmooth] = useState(true);
	const [reducedMotion, setReducedMotion] = useState(false);

	return (
		<fieldset className="flex flex-col gap-3">
			<legend className="label">Motion</legend>
			<Switch
				label="Smooth scrolling"
				checked={smooth}
				onChange={(event) => setSmooth(event.target.checked)}
			/>
			<Switch
				label="Reduce motion"
				checked={reducedMotion}
				onChange={(event) => setReducedMotion(event.target.checked)}
			/>
		</fieldset>
	);
}
```

## What this example is not

Each switch here changes its own setting immediately - neither is wired to
a submit button. That immediacy is what `role="switch"` promises; a form
that needs a confirm step before anything takes effect should use
`Checkbox` instead, not a `Switch` gated behind a button.
