Switch
Switches enable users to instantly activate or deactivate features, settings, or options with immediate visual confirmation. The component features a sliding thumb that moves along a track to communicate the current state.
import {Switch} from "@qualcomm-ui/react/switch"Examples
Simple
Basic switch using the simple API with a label.
<Switch label="Switch" />
Composite
Build with the composite API for granular control. This API requires you to provide each subcomponent, but gives you full control over the structure and layout.
<Switch.Root>
<Switch.HiddenInput />
<Switch.Label>Label</Switch.Label>
<Switch.Control />
</Switch.Root>
States
Based on the inputs, the switch will render as checked or unchecked.
<Switch aria-label="Toggle" />
<Switch aria-label="Toggle" defaultChecked />
Disabled State
When disabled is true, the switch becomes non-interactive and is rendered with reduced opacity to indicate its unavailable state.
<Switch defaultChecked disabled label="Disabled" />
<Switch disabled label="Disabled" />
Sizes
Use the size prop to change the size of the switch. The default size is md.
<Switch label="Small (sm)" size="sm" />
<Switch label="Medium (md)" size="md" />
<Switch label="Large (lg)" size="lg" />
Hint
Add a hint to provide additional context below the switch.
<Switch hint="You can change this later" label="Enable notifications" />
Group
Use SwitchGroup to group related switches with a shared label, hint text, and validation state.
Controlled State
Set the initial value using the defaultChecked prop, or use checked and onCheckedChange to control the value manually. These props follow our controlled state pattern.
import {type ReactElement, useState} from "react"
import {Switch} from "@qualcomm-ui/react/switch"
export function SwitchControlledDemo(): ReactElement {
const [checked, setChecked] = useState<boolean>(false)
return (
<Switch.Root
checked={checked}
onCheckedChange={(nextState) => {
console.log("Switch state change:", nextState)
setChecked(nextState)
}}
>
<Switch.HiddenInput />
<Switch.Control />
<Switch.Label>Label</Switch.Label>
</Switch.Root>
)
}Forms
Choose the form library that fits your needs—we've built examples with React Hook Form and Tanstack Form to get you started.
React Hook Form
Use React Hook Form to handle the switch state and validation. ArkType works great for schema validation if you need it.
import type {ReactElement} from "react"
import {arktypeResolver} from "@hookform/resolvers/arktype"
import {type} from "arktype"
import {Controller, useForm} from "react-hook-form"
import {Button} from "@qualcomm-ui/react/button"
import {Switch} from "@qualcomm-ui/react/switch"
interface FormData {
acceptTerms: boolean
newsletter: boolean
}
const acceptTermsSchema = type("boolean")
.narrow((value: boolean) => value === true)
.configure({
message: "Please accept the Terms of Service to continue",
})
const FormSchema = type({
// must be true
acceptTerms: acceptTermsSchema,
// can be true or false
newsletter: "boolean",
})
export function SwitchReactHookFormDemo(): ReactElement {
const {control, handleSubmit} = useForm<FormData>({
defaultValues: {
acceptTerms: false,
newsletter: true,
},
resolver: arktypeResolver(FormSchema),
})
return (
<form
className="flex w-56 flex-col gap-2"
onSubmit={(e) => {
void handleSubmit((data) => console.log(data))(e)
}}
>
<Controller
control={control}
name="newsletter"
render={({field: {name, onChange, value, ...fieldProps}}) => {
return (
<Switch.Root
checked={value}
name={name}
onCheckedChange={onChange}
{...fieldProps}
>
<Switch.HiddenInput />
<Switch.Control />
<Switch.Label>Subscribe to our Newsletter</Switch.Label>
</Switch.Root>
)
}}
/>
<Controller
control={control}
name="acceptTerms"
render={({
field: {onChange, value, ...fieldProps},
fieldState: {error},
}) => {
return (
<Switch.Root
checked={value}
invalid={!!error?.message}
onCheckedChange={onChange}
{...fieldProps}
>
<Switch.HiddenInput />
<Switch.Control />
<Switch.Label>Accept Terms of Service</Switch.Label>
<Switch.ErrorText>{error?.message}</Switch.ErrorText>
</Switch.Root>
)
}}
/>
<Button
className="mt-1"
emphasis="primary"
size="sm"
type="submit"
variant="fill"
>
Submit
</Button>
</form>
)
}Tanstack Form
Tanstack Form handles switch validation with its built-in validators.
import type {ReactElement} from "react"
import {useForm} from "@tanstack/react-form"
import {Button} from "@qualcomm-ui/react/button"
import {Switch} from "@qualcomm-ui/react/switch"
interface FormData {
acceptTerms: boolean
newsletter: boolean
}
const defaultFormData: FormData = {
acceptTerms: false,
newsletter: true,
}
const errorMessage = "Please accept the Terms of Service to continue"
export function SwitchTanstackFormDemo(): ReactElement {
const form = useForm({
defaultValues: defaultFormData,
onSubmit: ({value}) => {
// Do something with form data
console.log(value)
},
})
return (
<form
className="flex w-56 flex-col gap-2"
onSubmit={(event) => {
event.preventDefault()
event.stopPropagation()
void form.handleSubmit()
}}
>
<form.Field name="newsletter">
{({handleChange, name, state}) => {
return (
<Switch.Root
checked={state.value}
name={name}
onCheckedChange={handleChange}
>
<Switch.HiddenInput />
<Switch.Control />
<Switch.Label>Subscribe to our Newsletter</Switch.Label>
</Switch.Root>
)
}}
</form.Field>
<form.Field
name="acceptTerms"
validators={{
onChange: (field) => (field.value ? undefined : errorMessage),
onSubmit: (field) => (field.value ? undefined : errorMessage),
}}
>
{({handleChange, name, state}) => {
const fieldError =
state.meta.errorMap["onChange"] || state.meta.errorMap["onSubmit"]
return (
<Switch.Root
checked={state.value}
invalid={!!fieldError}
name={name}
onCheckedChange={handleChange}
>
<Switch.HiddenInput />
<Switch.Control />
<Switch.Label>Accept Terms</Switch.Label>
<Switch.ErrorText>{fieldError}</Switch.ErrorText>
</Switch.Root>
)
}}
</form.Field>
<Button
className="mt-1"
emphasis="primary"
size="sm"
type="submit"
variant="fill"
>
Submit
</Button>
</form>
)
}Tanstack form also supports ArkType.
Composite Guidelines
The composite elements are only intended to be used as direct descendants of the <Switch.Root> component.
/* Won't work alone ❌ */
<Switch.HiddenInput />
<Switch.Control />
/* Works as expected ✅ */
<Switch.Root>
<Switch.HiddenInput />
<Switch.Control />
</Switch.Root>Shortcuts
Control and Thumb
The switch control automatically renders the thumb if no child elements are supplied. This:
<Switch.Control />is equivalent to this:
<Switch.Control>
<Switch.Thumb />
</Switch.Control>Explorer
API
<Switch>
The Switch component supports all props from Switch.Root, plus the additional props listed below.
stringstring{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
{
children?: ReactNode
icon?:
| LucideIcon
| ReactNode
id?: string
render?:
| Element
| ((
props: Props,
) => Element)
}
any{
children?: ReactNode
id?: string
render?:
| Element
| ((
props: Props,
) => Element)
}
{
children?: ReactNode
id?: string
render?:
| Element
| ((
props: Props,
) => Element)
}
{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
Composite API
<Switch.Root>
booleanboolean'ltr' | 'rtl'
booleanstring() =>
| Node
| ShadowRoot
| Document
Partial<{
errorText: string
hiddenInput: string
hint: string
label: string
root: string
}>
booleanstring(
checked: boolean,
) => void
(
focused: boolean,
) => void
boolean| ReactElement
| ((
props: object,
) => ReactElement)
boolean| 'sm'
| 'md'
| 'lg'
stringclassName'qui-switch__root'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-readonlydata-size| 'sm'
| 'md'
| 'lg'
data-state| 'checked'
| 'unchecked'
data-switch-part'root'<Switch.Label>
<span> element by default.string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-switch__label'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-readonlydata-size| 'sm'
| 'md'
| 'lg'
data-state| 'checked'
| 'unchecked'
data-switch-part'label'<Switch.Control>
<span> element by
default.| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-switch__control'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-readonlydata-size| 'sm'
| 'md'
| 'lg'
data-state| 'checked'
| 'unchecked'
data-switch-part'control'<Switch.Thumb>
<span> element by
default.| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-switch__thumb'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-readonlydata-size| 'sm'
| 'md'
| 'lg'
data-state| 'checked'
| 'unchecked'
data-switch-part'thumb'<Switch.Hint>
<div> element by
default.string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-input__hint'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-readonlydata-state| 'checked'
| 'unchecked'
data-switch-part'hint'hiddenboolean<Switch.ErrorText>
<div> element by
default.| LucideIcon
| ReactNode
string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-input__error-text'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-readonlydata-state| 'checked'
| 'unchecked'
data-switch-part'error-text'hiddenboolean<Switch.HiddenInput>
<input> element. Note: do not apply typical input props like disabled
to this element. Those are applied to the root element and propagated via
internal context.className'qui-switch__hidden-input'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-readonlydata-state| 'checked'
| 'unchecked'
data-switch-part'hidden-input'style
<label>element by default.