Switch
Switch: anatomy, keyboard behavior, state, and theming in Askr.
Example
import { Field, FieldLabel, Switch } from '@askrjs/themes/components';
<Field>
<Switch id="public" checked={isPublic()} onCheckedChange={setIsPublic} />
<FieldLabel htmlFor="public">Public project</FieldLabel>
</Field>Toggle a boolean setting.
Published props
Generated from the TypeScript declarations shipped by the installed package. Named types in the Type column define the accepted values.
SwitchAsChildProps
Import from @askrjs/ui/switch.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
checked | checked?: boolean | undefined; | — | — |
children | children: JSXElement; | — | — |
defaultChecked | defaultChecked?: boolean | undefined; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
name | name?: string | undefined; | — | — |
onCheckedChange | onCheckedChange?: ((checked: boolean) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
required | required?: boolean | undefined; | — | — |
type | type?: undefined; | — | — |
value | value?: string | undefined; | — | — |
SwitchButtonProps
Import from @askrjs/ui/switch.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
checked | checked?: boolean | undefined; | — | — |
children | children?: unknown; | — | — |
defaultChecked | defaultChecked?: boolean | undefined; | — | — |
onCheckedChange | onCheckedChange?: ((checked: boolean) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
required | required?: boolean | undefined; | — | — |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
SwitchOwnProps
Import from @askrjs/ui/switch.
| Prop | Type | Default | Description |
|---|---|---|---|
checked | checked?: boolean | undefined; | — | — |
children | children?: unknown; | — | — |
defaultChecked | defaultChecked?: boolean | undefined; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
name | name?: string | undefined; | — | — |
onCheckedChange | onCheckedChange?: ((checked: boolean) => void) | undefined; | — | — |
required | required?: boolean | undefined; | — | — |
value | value?: string | undefined; | — | — |
SwitchProps
Import from @askrjs/ui/switch.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: boolean | undefined; | — | — |
checked | checked?: boolean | undefined; | — | — |
children | children?: unknown; | — | — |
defaultChecked | defaultChecked?: boolean | undefined; | — | — |
onCheckedChange | onCheckedChange?: ((checked: boolean) => void) | undefined; | — | — |
ref | ref?: ((value: Element | null) => void) | { current: Element | null; } | ((value: HTMLButtonElement | null) => void) | { current: HTMLButtonElement | null; } | null | undefined; | — | — |
required | required?: boolean | undefined; | — | — |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
Purpose
Switch is a binary on/off control that behaves like a checkbox but reads as a toggle — think settings screens where flipping a switch has an immediate effect rather than being one of several fields submitted together. Use it when the choice is genuinely binary and the change should feel instant, not when you need a labeled yes/no inside a longer form (Checkbox usually reads better there).
Install and import
Import the behavior from `@askrjs/ui/switch` and the default appearance from `@askrjs/themes/switch`. Both are real subpath exports of their respective packages, so you never need to reach into the root barrel just to render a switch.
Live examples
The demos cover a plain uncontrolled switch, a controlled one wired to `checked`/`onCheckedChange`, and a disabled state. Each renders as a native `<button>` under the hood, which is worth noticing if you're inspecting the DOM — there's no hidden checkbox unless you also pass `name`.
Anatomy
Switch has no sub-parts — it is a single control, unlike Accordion or Select. It renders as a button carrying `role="switch"` and `aria-checked`, and takes `checked` with `onCheckedChange` for controlled use or `defaultChecked` for uncontrolled. `name` and `value` make it submit inside a plain form, and `required` and `disabled` behave as they would on a native input. Pair it with `Field` from `@askrjs/themes/field` and `FieldLabel` from the `@askrjs/themes/components` barrel (it isn't exported from `/field` itself) rather than a bare `label`, so the description and error slots line up with the rest of your form controls.
State model
`checked` can be left uncontrolled via `defaultChecked`, or driven externally by passing `checked` alongside `onCheckedChange`. There's no separate internal boolean to fight with: whatever value you supply (or the current default) is exactly what renders, and every interaction routes through `onCheckedChange` before anything changes visually in a controlled setup.
Keyboard and accessibility
Switch renders with `role="switch"` and `aria-checked`, and both Enter and Space toggle it — the exact contract spelled out in the package's `SWITCH_A11Y_CONTRACT`, which mirrors the switch pattern published at w3.org/WAI/ARIA/apg/patterns/switch. When disabled, native `<button disabled>` semantics apply automatically since the default render target is a real button element.
Styling and tokens
The track is styled through `[data-slot="switch"]` and `data-state` (`checked`/`unchecked`) rather than boolean class toggles, so a themed override can target `[data-state="checked"]` without needing to know which internal class name maps to which state. The thumb has no `data-slot` of its own — it's a `::after` pseudo-element on the track, styled with `[data-slot="switch"]::after`, not a separate DOM node you can target directly. `data-disabled` is set on the track the same way for the disabled visual treatment.
API
`Switch` accepts `checked`, `defaultChecked`, `onCheckedChange`, `disabled`, `required`, `name`, and `value` — the last two exist specifically for the `asChild=false` case where the component ends up participating in native form submission. When `asChild` is true, `Switch` renders through `Slot` onto your own element instead of a `<button>`, and `type` is not accepted in that mode since there's no native button to type.
Edge cases
Per the package's `FORM_INTEGRATION` contract, Switch submits through a hidden `checkbox`-type input with value `"on"` when `name` is set — the same convention native checkboxes use, so existing form-handling code that expects checkbox-style submission keeps working. That hidden input renders unconditionally off `name` being set, independent of `asChild` — rendering via `asChild` onto a non-button element doesn't remove it. `aria-disabled` handling is likewise automatic either way: it comes from the same `pressable()` foundation call regardless of which branch renders, so you don't need to wire it yourself when using `asChild`. What `asChild` does remove is the native `disabled` attribute path specifically, since a non-button host has no such attribute to set.
Related pages
If the option is one of several related boolean settings displayed together, compare against Checkbox, which typically reads better in dense lists. For a two-state control that toggles a pressed/unpressed visual rather than a settings value, see Toggle Family instead.