Switch
An on and off switch for a setting that takes effect at once. Its thumb travels the right way in right-to-left layouts.
client - size not measured yet
Switch
client - size not measured yetAn on and off switch for a setting that takes effect at once. Its thumb travels the right way in right-to-left layouts.
Try it
size
invalid
The code
switch.tsx
import { Switch } from 'topclassui/switch'
<div className="flex items-center gap-2">
<Switch id="switch-playground" defaultChecked />
<label htmlFor="switch-playground" className="text-sm text-body">Weekly digest</label>
</div>The xs and sm switches still grow to a 44px target under a touch pointer, so the thumb stays easy to tap.
Every state
7 statesImport from the package
topclassui/switch
import { Switch } from 'topclassui/switch'Install once:npm i topclassui@beta zod
Examples
- For a setting that takes effect as it is turned on or off. For a choice the form sends later, a Checkbox reads better.
- In a form, off sends nothing and on sends on, before the page hydrates and after. An unchecked value makes an off switch send a string of its own.
- A form reset brings the switch back to its default, on screen and in the form.
- Switch draws no label of its own: write a
<label htmlFor>that points at itsid. size="custom"draws no track size: give the track one from your own CSS.
basic
basic.tsx
import { Switch } from 'topclassui/switch'
/** Outside a Field: a native label points at the id of the switch, which is the button itself. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Switch id="switch-basic-digest" defaultChecked />
<label htmlFor="switch-basic-digest" className="text-sm text-body">Weekly digest</label>
</div>
<div className="flex items-center gap-2">
<Switch id="switch-basic-mentions" />
<label htmlFor="switch-basic-mentions" className="text-sm text-body">Email me when I am mentioned</label>
</div>
</div>
)
}
form
form.tsx
import { Button } from 'topclassui/button'
import { Switch } from 'topclassui/switch'
/** One switch that is on, in a plain form: it sends digest=on, before any script runs and after. */
export default function Demo() {
return (
<form className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Switch id="switch-form-digest" name="digest" defaultChecked />
<label htmlFor="switch-form-digest" className="text-sm text-body">Weekly digest</label>
</div>
<Button type="submit">Save</Button>
</form>
)
}
unchecked-value
unchecked-value.tsx
import { Switch } from 'topclassui/switch'
/** A switch that is off still sends a value: Base UI adds an input of type hidden beside it. */
export default function Demo() {
return (
<form className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Switch id="switch-unchecked-news" name="news" value="yes" uncheckedValue="no" />
<label htmlFor="switch-unchecked-news" className="text-sm text-body">Product news</label>
</div>
</form>
)
}
sizes
sizes.tsx
import { Switch } from 'topclassui/switch'
/** The five sizes, on. Each target stays 24 px, and 44 px under a finger, whatever the track. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-4">
<Switch size="xs" defaultChecked aria-label="Extra small" />
<Switch size="sm" defaultChecked aria-label="Small" />
<Switch size="md" defaultChecked aria-label="Medium" />
<Switch size="lg" defaultChecked aria-label="Large" />
<Switch size="xl" defaultChecked aria-label="Extra large" />
</div>
)
}
In Arabic, right to left
rtl.tsx
import { TopClassProvider } from 'topclassui/provider'
import { Switch } from 'topclassui/switch'
/** Arabic, inside TopClassProvider as the root layout would mount it. The thumb starts at the right and
* slides to the left when the switch is on. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Switch id="switch-rtl-digest" name="digest" defaultChecked />
<label htmlFor="switch-rtl-digest" className="text-sm text-body">الملخص الأسبوعي</label>
</div>
<div className="flex items-center gap-2">
<Switch id="switch-rtl-mentions" name="mentions" />
<label htmlFor="switch-rtl-mentions" className="text-sm text-body">راسلني عند الإشارة إليّ</label>
</div>
</div>
</TopClassProvider>
)
}
Props
Keyboard and accessibility
- A name is required:
idfor a<label htmlFor>, oraria-label, oraria-labelledby. The root is a<button role="switch">. - The target is 24px, 44px under a touch pointer, whatever
sizedraws: the focus ring is around the track. invalidsets aria-invalid, and the line of the track turns to danger and thickens.
Right to left
The thumb starts at the right and slides left when the switch is on, with no code change.
Server and client
A client component: it renders on the server and hydrates in the browser.
Files: index.tsx, client.tsx.
On the chip: client - size not measured yet.