Switch
A control that allows the user to toggle between checked and not checked.
import { css } from 'remix/component'
import { Label } from '#app/ui/label.tsx'
import { Switch } from '#app/ui/switch.tsx'
const rowStyles = css({import { css } from 'remix/component'
import { Label } from '#app/ui/label.tsx'
import { Switch } from '#app/ui/switch.tsx'
const rowStyles = css({
display: 'flex',
alignItems: 'center',
'& > :not(:last-child)': { marginInlineEnd: '0.5rem' },
})
export function SwitchDemo() {
return () => (
<div mix={rowStyles}>
<Switch id="airplane-mode" />
<Label htmlFor="airplane-mode">Airplane Mode</Label>
</div>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/switch.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/switch.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/switch.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/switch.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, on, ref, type Handle, type Props } from 'remix/component'
export interface SwitchProps extends Omit<Props<'button'>, 'value' | 'type'> {
/** Controlled checked state. */
checked?: boolean
/** Initial checked state when uncontrolled. */
defaultChecked?: boolean
onCheckedChange?: (checked: boolean) => void
required?: boolean
/** Value submitted with the form when checked. */
value?: string
size?: 'sm' | 'default'
}
const switchStyles = css({
display: 'inline-flex',
flexShrink: 0,
alignItems: 'center',
borderRadius: '9999px',
borderWidth: '1px',
borderColor: 'transparent',
'--tw-shadow': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
outlineStyle: 'none',
'&:focus-visible': {
borderColor: 'var(--ring)',
'--tw-ring-shadow':
'0 0 0 3px var(--tw-ring-color, color-mix(in oklab, var(--ring) 50%, transparent))',
},
'&:disabled': { cursor: 'not-allowed', opacity: 0.5 },
'&[data-size="default"]': { height: '1.15rem', width: '2rem' },
'&[data-size="sm"]': { height: '0.875rem', width: '1.5rem' },
'&[data-state="checked"]': { backgroundColor: 'var(--primary)' },
'&[data-state="unchecked"]': { backgroundColor: 'var(--input)' },
'.dark &[data-state="unchecked"]': {
backgroundColor: 'color-mix(in oklab, var(--input) 80%, transparent)',
},
})
const thumbStyles = css({
pointerEvents: 'none',
display: 'block',
borderRadius: '9999px',
backgroundColor: 'var(--background)',
transitionProperty: 'transform, translate, scale, rotate',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
'[data-slot="switch"][data-size="default"] &': { width: '1rem', height: '1rem' },
'[data-slot="switch"][data-size="sm"] &': { width: '0.75rem', height: '0.75rem' },
'&[data-state="checked"]': { translate: 'calc(100% - 2px) 0' },
'&[data-state="unchecked"]': { translate: '0 0' },
'.dark &[data-state="checked"]': { backgroundColor: 'var(--primary-foreground)' },
'.dark &[data-state="unchecked"]': { backgroundColor: 'var(--foreground)' },
})
/** The visually hidden native input that lets the switch take part in form submission. */
const bubbleInputStyles = css({
position: 'absolute',
transform: 'translateX(-100%)',
pointerEvents: 'none',
opacity: 0,
margin: 0,
})
/**
* A control that allows the user to toggle between checked and not checked. Renders a
* `<button role="switch">`; when it belongs to a form, a hidden native checkbox mirrors its
* state so `name`/`value`/`required` work with native form submission and validation.
*/
export function Switch(handle: Handle<SwitchProps>) {
let uncontrolled = handle.props.defaultChecked ?? false
let initial = uncontrolled
let isFormControl = true
let bubbleInput: HTMLInputElement | undefined
async function setChecked(next: boolean) {
if (handle.props.checked === undefined) uncontrolled = next
handle.props.onCheckedChange?.(next)
await handle.update()
// Let form-level listeners observe the change like a native checkbox.
bubbleInput?.dispatchEvent(new Event('input', { bubbles: true }))
bubbleInput?.dispatchEvent(new Event('change', { bubbles: true }))
}
return () => {
let {
checked: _checked,
defaultChecked: _defaultChecked,
onCheckedChange: _onCheckedChange,
size = 'default',
required,
name,
value = 'on',
disabled,
form,
mix,
...props
} = handle.props
let checked = handle.props.checked ?? uncontrolled
let state = checked ? 'checked' : 'unchecked'
return (
<>
<button
type="button"
role="switch"
aria-checked={checked}
aria-required={required}
data-state={state}
data-disabled={disabled ? '' : undefined}
disabled={disabled}
value={value}
form={form}
data-slot="switch"
data-size={size}
class="peer"
{...props}
mix={[
switchStyles,
ref((node, signal) => {
let owner = node.form
if (!owner) {
isFormControl = false
queueMicrotask(() => handle.update())
return
}
owner.addEventListener('reset', () => setChecked(initial), { signal })
}),
on('click', () => {
setChecked(!(handle.props.checked ?? uncontrolled))
}),
mix,
]}
>
<span
data-slot="switch-thumb"
data-state={state}
data-disabled={disabled ? '' : undefined}
mix={thumbStyles}
/>
</button>
{isFormControl && (
<input
type="checkbox"
aria-hidden="true"
tabIndex={-1}
name={name}
value={value}
checked={checked}
required={required}
disabled={disabled}
form={form}
mix={[
bubbleInputStyles,
ref((node) => {
bubbleInput = node
let control = node.previousElementSibling as HTMLElement | null
if (control) {
// `important` so layout rules like Field's `[&>*]:w-full` can't resize it.
node.style.setProperty('width', `${control.offsetWidth}px`, 'important')
node.style.setProperty('height', `${control.offsetHeight}px`, 'important')
}
}),
]}
/>
)}
</>
)
}
}Update the import paths to match your project setup.
Usage
import { Switch } from '#app/ui/switch.tsx'<Switch />API Reference
Switch
A control that allows the user to toggle between checked and not checked. Renders a <button role="switch">; when it belongs to a form, a hidden native checkbox mirrors its state so name/value/required work with native form submission and validation.
| Prop | Type | Default |
|---|---|---|
checked | boolean | - |
defaultChecked | boolean | - |
onCheckedChange | ((checked: boolean) => void) | - |
required | boolean | - |
value | string | "on" |
size | "default" | "sm" | "default" |
mix | MixValue | - |