Crustack

@crustack/checkbox

Build performant & accessible Checkbox components.

Features

  • Works with native <form>
  • Full keyboard navigation.
  • Can be controlled or uncontrolled.
  • Indeterminate support (requires "use client")

Installation

npm install @crustack/form

Basic Example

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

:::caution To take full advantage of the "peer" selector, ensure the order of the elements remains unchanged. :::

checkbox.tsx
import {  } from '@crustack/checkbox'
import {  as  } from 'tailwind-merge'
import {  } from 'lucide-react'

const  = ({
  : '0.5rem', // the clickable area is 0.5rem larger than the checkbox
  : 'pointer',
})

type  = React.<'input'>

export function ({ , , ... }: ) {
  return (
    <. ={('size-5', )} ={}>
      {/* Hidden input, spread to props here */}
      {/* hitboxPadding & cursorStyle are applied on this element */}
      <. {...} ="peer" />

      {/* The visible part */}
      {/* `checkbox.Box` size is 100% of `checkbox.Root` size */}
      <.
        ={(
          // 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-2 peer-focus-visible:outline-current',
          // invalid styles
          'peer-data-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 */}
        <.>
          < ="size-3/5 transition-all" />
        </.>
      </.>
    </.>
  )
}

With inderterminate

checkbox.tsx
+ "use client"
  import { defineCheckbox } from '@crustack/checkbox'
  import { twMerge as cn } from 'tailwind-merge'
  import { Check } from 'lucide-react'

  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-2 peer-focus-visible:outline-current',
            // invalid styles
            'peer-data-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>
            <Check className="size-3/5 transition-all" />
          </checkbox.Icon>
        </checkbox.Box>
      </checkbox.Root>
    )
  }

API Reference

👉 defineCheckbox

DefineCheckboxOptions

PropTypeDefault
cursorStyle?
string | false
'pointer'
hitboxPadding?
string | number
0

👉 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

PropTypeDefault
(1) checked?
boolean | "indeterminate"
-

Returns

PropTypeDefault
checked?
boolean
-
ref?
React.RefObject<HTMLInputElement | null>
-

On this page