Skip to content

Checkbox

Size
0.61 kb
View source

Headless components for building accessible checkbox elements.

Features

  • Works with native <form>
  • Full keyboard navigation.
  • Can be controlled or uncontrolled.
  • Can be fully rendered on the server (no client-side code)
  • Indeterminate support (requires “use client”)

Basic Example

You don’t need to worry about handling the positioning or accessibility of the elements. Just focus on styling them as needed.

checkbox.tsx
import { defineCheckbox } from 'crustack/checkbox'
import { cn } from 'crustack/utils'
import { FaCheck } from 'react-icons/fa6'
const checkbox = defineCheckbox({
hitboxPadding: '0.5rem', // the clickable area is 0.5rem larger than the checkbox
cursorStyle: 'pointer',
})
type Props = React.ComponentPropsWithoutRef<'input'>
export function Checkbox({ className, style, ...props }: Props) {
return (
<checkbox.Root className={cn(className, 'size-5')} style={style}>
{/* Hidden input, spread to props here */}
{/* hitboxPadding & cursorStyle are applied on this element */}
<checkbox.HiddenInput {...props} className="peer" />
{/* The visible part */}
{/* `checkbox.Box` size is 100% of `checkbox.Root` size */}
<checkbox.Box
className={cn(
// base styles
'pointer-events-none rounded border border-current transition-all [&_svg]:scale-0',
// hover styles
'peer-hover:bg-base-200',
// focus visible styles
'peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-current',
// invalid styles
'peer-aria-invalid:text-error',
// disabled styles
'peer-disabled:opacity-50 peer-disabled:grayscale',
// checked styles
'peer-checked:text-teal-500 peer-checked:[&_svg]:scale-100',
)}
>
{/* `checkbox.Icon` positions it's children properly */}
<checkbox.Icon>
<FaCheck className="size-3/5 transition-all" />
</checkbox.Icon>
</checkbox.Box>
</checkbox.Root>
)
}

With inderterminate

checkbox.tsx
"use client"
import { defineCheckbox } from 'crustack/checkbox'
import { cn } from 'crustack/utils'
import { FaCheck } from 'react-icons/fa6'
const checkbox = defineCheckbox({
hitboxPadding: '0.5rem', // the clickable area is 0.5rem larger than the checkbox
})
type Props = React.ComponentPropsWithoutRef<'input'>
type Props = React.ComponentPropsWithoutRef<'input'> & {
checked: boolean | 'indeterminate'
}
export function Checkbox({ className, style ...props }: Props) {
const { checked, ref} = checkbox.useIndeterminate(props.checked)
return (
<checkbox.Root className={cn(className, 'size-5')} style={style}>
{/* Hidden input, spread to props here */}
{/* hitboxPadding & cursorStyle are applied on this element */}
<checkbox.HiddenInput {...props} className="peer" />
<checkbox.HiddenInput {...props} className="peer" checked={checked} ref={ref} />
{/* The visible part */}
{/* `checkbox.Box` size is 100% of `checkbox.Root` size */}
<checkbox.Box
className={cn(
// base styles
'pointer-events-none rounded border border-current transition-all [&_svg]:scale-0',
// hover styles
'peer-hover:bg-base-200',
// focus visible styles
'peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-current',
// invalid styles
'peer-aria-invalid:text-error',
// disabled styles
'peer-disabled:opacity-50 peer-disabled:grayscale',
// indeterminate styles
'peer-indeterminate:...',
// checked styles
'peer-checked:text-teal-500 peer-checked:[&_svg]:scale-100',
)}>
{/* `checkbox.Icon` positions it's children properly */}
<checkbox.Icon>
<FaCheck className="size-3/5 transition-all" />
</checkbox.Icon>
</checkbox.Box>
</checkbox.Root>
)
}

API Reference

defineCheckbox

1st argumenttypedefaultdescription
config.hitboxPaddingstring | number0Enhance the accessibility of the checkbock by increasing its hitbox.
config.cursorStylestring | false'pointer'The cursor style of the checkbox, applied to the hitbox area.

checkbox.Root

The top-level element, which accepts the same props as a standard <div>.

Sizing this element will automatically size all child elements.

checkbox.HiddenInput

The visually hidden input element, which accepts the same props as a standard <input>.

Styling is handled automatically, so you don’t need to apply any additional styles.

checkbox.Box

The visual part of the checkbox, which accepts the same props as a standard <div>.

For proper styling, it should be the next sibling of the HiddenInput.

Its size is inherited from the Root element.

checkbox.Icon

The container for the icon of the checkbox, which accepts the same props as a standard <div>.

It automatically centers its child within the Box element.

checkbox.useIndeterminate

A utility hook for managing the indeterminate state of the checkbox.

Arguments

1st argumenttypedescription
checkedboolean | 'indeterminate'The current state of the checkbox

Returns

These values should be passed as props to the HiddenInput:

returnsdescription
refA reference to attach to the HiddenInput
checkedThe state of the HiddenInput