Checkbox
A control that allows the user to toggle between checked and not checked.
By clicking this checkbox, you agree to the terms and conditions.
import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
import { Label } from '#app/ui/label.tsx'
const stackStyles = css({ display: 'flex', flexDirection: 'column', gap: '1.5rem' })import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
import { Label } from '#app/ui/label.tsx'
const stackStyles = css({ display: 'flex', flexDirection: 'column', gap: '1.5rem' })
const rowCenterStyles = css({ display: 'flex', alignItems: 'center', gap: '0.75rem' })
const rowStartStyles = css({ display: 'flex', alignItems: 'flex-start', gap: '0.75rem' })
const textStackStyles = css({ display: 'grid', gap: '0.5rem' })
const mutedStyles = css({
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
color: 'var(--muted-foreground)',
})
const cardStyles = css({
display: 'flex',
alignItems: 'flex-start',
gap: '0.75rem',
borderRadius: 'var(--radius)',
borderWidth: '1px',
padding: '0.75rem',
'&:hover': { backgroundColor: 'color-mix(in oklab, var(--accent) 50%, transparent)' },
'&:has([aria-checked="true"])': {
borderColor: 'oklch(54.6% 0.245 262.881)',
backgroundColor: 'oklch(97% 0.014 254.604)',
},
'.dark &:has([aria-checked="true"])': {
borderColor: 'oklch(37.9% 0.146 265.522)',
backgroundColor: 'oklch(28.2% 0.091 267.935)',
},
})
const blueCheckboxStyles = css({
'&[data-state="checked"]': {
borderColor: 'oklch(54.6% 0.245 262.881)',
backgroundColor: 'oklch(54.6% 0.245 262.881)',
color: 'white',
},
'.dark &[data-state="checked"]': {
borderColor: 'oklch(48.8% 0.243 264.376)',
backgroundColor: 'oklch(48.8% 0.243 264.376)',
},
})
const cardTextStyles = css({ display: 'grid', gap: '0.375rem', fontWeight: 400 })
const cardTitleStyles = css({ fontSize: '0.875rem', lineHeight: 1, fontWeight: 500 })
export function CheckboxDemo() {
return () => (
<div mix={stackStyles}>
<div mix={rowCenterStyles}>
<Checkbox id="terms" />
<Label htmlFor="terms">Accept terms and conditions</Label>
</div>
<div mix={rowStartStyles}>
<Checkbox id="terms-2" defaultChecked />
<div mix={textStackStyles}>
<Label htmlFor="terms-2">Accept terms and conditions</Label>
<p mix={mutedStyles}>By clicking this checkbox, you agree to the terms and conditions.</p>
</div>
</div>
<div mix={rowStartStyles}>
<Checkbox id="toggle" disabled />
<Label htmlFor="toggle">Enable notifications</Label>
</div>
<Label mix={cardStyles}>
<Checkbox id="toggle-2" defaultChecked mix={blueCheckboxStyles} />
<div mix={cardTextStyles}>
<p mix={cardTitleStyles}>Enable notifications</p>
<p mix={mutedStyles}>You can enable or disable notifications at any time.</p>
</div>
</Label>
</div>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/checkbox.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/checkbox.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/checkbox.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/checkbox.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'
import { CheckIcon } from './icons.tsx'
export type CheckedState = boolean | 'indeterminate'
export interface CheckboxProps extends Omit<Props<'button'>, 'value' | 'type'> {
/** Controlled checked state. */
checked?: CheckedState
/** Initial checked state when uncontrolled. */
defaultChecked?: CheckedState
onCheckedChange?: (checked: CheckedState) => void
required?: boolean
/** Value submitted with the form when checked. */
value?: string
}
const checkboxStyles = css({
width: '1rem',
height: '1rem',
flexShrink: 0,
borderRadius: '4px',
borderWidth: '1px',
borderColor: 'var(--input)',
'--tw-shadow': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
transitionProperty: 'box-shadow',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
outlineStyle: 'none',
'.dark &': { backgroundColor: 'color-mix(in oklab, var(--input) 30%, transparent)' },
'&: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 },
'&[aria-invalid="true"]': {
borderColor: 'var(--destructive)',
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 20%, transparent)',
},
'.dark &[aria-invalid="true"]': {
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 40%, transparent)',
},
'&[data-state="checked"]': {
borderColor: 'var(--primary)',
backgroundColor: 'var(--primary)',
color: 'var(--primary-foreground)',
},
'.dark &[data-state="checked"]': { backgroundColor: 'var(--primary)' },
})
const indicatorStyles = css({
display: 'grid',
placeContent: 'center',
color: 'currentcolor',
transitionProperty: 'none',
pointerEvents: 'none',
})
const iconStyles = css({ width: '0.875rem', height: '0.875rem' })
/** The visually hidden native input that lets the checkbox 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="checkbox">`; 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 Checkbox(handle: Handle<CheckboxProps>) {
let uncontrolled: CheckedState = handle.props.defaultChecked ?? false
let initial = uncontrolled
let isFormControl = true
let bubbleInput: HTMLInputElement | undefined
let current = () => handle.props.checked ?? uncontrolled
async function setChecked(next: CheckedState) {
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,
required,
name,
value = 'on',
disabled,
form,
mix,
...props
} = handle.props
let checked = current()
let state = checked === 'indeterminate' ? 'indeterminate' : checked ? 'checked' : 'unchecked'
handle.queueTask(() => {
if (bubbleInput) bubbleInput.indeterminate = checked === 'indeterminate'
})
return (
<>
<button
type="button"
role="checkbox"
aria-checked={checked === 'indeterminate' ? 'mixed' : checked}
aria-required={required}
data-state={state}
data-disabled={disabled ? '' : undefined}
disabled={disabled}
value={value}
form={form}
data-slot="checkbox"
class="peer"
{...props}
mix={[
checkboxStyles,
ref((node, signal) => {
let owner = node.form
if (!owner) {
isFormControl = false
queueMicrotask(() => handle.update())
return
}
owner.addEventListener('reset', () => setChecked(initial), { signal })
}),
on('keydown', (event) => {
// WAI-ARIA: checkboxes don't activate on Enter.
if (event.key === 'Enter') event.preventDefault()
}),
on('click', () => {
setChecked(current() === 'indeterminate' ? true : !current())
}),
mix,
]}
>
{checked !== false && (
<span
data-slot="checkbox-indicator"
data-state={state}
data-disabled={disabled ? '' : undefined}
mix={indicatorStyles}
>
<CheckIcon mix={iconStyles} />
</span>
)}
</button>
{isFormControl && (
<input
type="checkbox"
aria-hidden="true"
tabIndex={-1}
name={name}
value={value}
checked={checked === true}
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')
}
node.indeterminate = current() === 'indeterminate'
}),
]}
/>
)}
</>
)
}
}Update the import paths to match your project setup.
Usage
import { Checkbox } from '#app/ui/checkbox.tsx'<Checkbox />Examples
Disabled
Use the disabled prop to prevent interaction and add the data-disabled attribute to the <Field> component for disabled styles.
import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
const rowStyles = css({
display: 'flex',import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
const rowStyles = css({
display: 'flex',
alignItems: 'center',
'& > :not(:last-child)': { marginInlineEnd: '0.5rem' },
})
const labelStyles = css({
fontSize: '0.875rem',
lineHeight: 1,
fontWeight: 500,
'.peer:disabled ~ &': { cursor: 'not-allowed', opacity: 0.7 },
})
export function CheckboxDisabled() {
return () => (
<div mix={rowStyles}>
<Checkbox id="terms2" disabled />
<label htmlFor="terms2" mix={labelStyles}>
Accept terms and conditions
</label>
</div>
)
}With text
You agree to our Terms of Service and Privacy Policy.
import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
const rowStyles = css({ display: 'flex', gap: '0.5rem' })
const textStyles = css({ display: 'grid', gap: '0.375rem', lineHeight: 1 })import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
const rowStyles = css({ display: 'flex', gap: '0.5rem' })
const textStyles = css({ display: 'grid', gap: '0.375rem', lineHeight: 1 })
const labelStyles = css({
fontSize: '0.875rem',
lineHeight: 1,
fontWeight: 500,
'.peer:disabled ~ &': { cursor: 'not-allowed', opacity: 0.7 },
})
const mutedStyles = css({
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
color: 'var(--muted-foreground)',
})
export function CheckboxWithText() {
return () => (
<div mix={rowStyles}>
<Checkbox id="terms1" />
<div mix={textStyles}>
<label htmlFor="terms1" mix={labelStyles}>
Accept terms and conditions
</label>
<p mix={mutedStyles}>You agree to our Terms of Service and Privacy Policy.</p>
</div>
</div>
)
}API Reference
Checkbox
A control that allows the user to toggle between checked and not checked. Renders a <button role="checkbox">; 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 | CheckedState | - |
defaultChecked | CheckedState | - |
onCheckedChange | ((checked: CheckedState) => void) | - |
required | boolean | - |
value | string | "on" |
mix | MixValue | - |