rapidUI

Checkbox

Accessible checkbox with checked, unchecked and indeterminate states.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/checkbox.json

Installs to @/components/ui/checkbox.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0 lucide-react@^1.48.0

Copy the source into your project:

components/ui/checkbox.tsx
'use client'

import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'
import { CheckIcon, MinusIcon } from 'lucide-react'
import { cn } from '@/lib/utils'

/**
 * `readOnly` shows the value but prevents changing it. Unlike `disabled`, the checkbox stays focusable and is announced
 * as read-only (`aria-readonly`). Base UI blocks the change and sets `data-readonly`; we also keep `data-read-only`.
 */
function Checkbox({ className, readOnly, ...props }: CheckboxPrimitive.Root.Props) {
	return (
		<CheckboxPrimitive.Root
			data-slot="checkbox"
			data-read-only={readOnly || undefined}
			readOnly={readOnly}
			className={cn(
				'peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary data-readonly:cursor-default data-readonly:border-dashed data-readonly:border-foreground/60 data-readonly:bg-muted/50 data-readonly:data-checked:border-foreground/60 data-readonly:data-checked:bg-muted/50 data-readonly:data-checked:text-foreground data-readonly:data-indeterminate:border-foreground/60 data-readonly:data-indeterminate:bg-muted/50 data-readonly:data-indeterminate:text-foreground',
				className,
			)}
			{...props}
		>
			<CheckboxPrimitive.Indicator
				data-slot="checkbox-indicator"
				className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
			>
				{/* A minus for the indeterminate state, so it isn't shown as checked */}
				<CheckIcon aria-hidden="true" className="in-data-indeterminate:hidden" />
				<MinusIcon aria-hidden="true" className="hidden in-data-indeterminate:block" />
			</CheckboxPrimitive.Indicator>
		</CheckboxPrimitive.Root>
	)
}

export { Checkbox }

A checkbox component that allows users to select one or multiple options. Built on top of Base UI Checkbox, it follows WAI-ARIA design patterns and is fully accessible.

Features

  • Checked and unchecked states, controlled (checked / onCheckedChange) or uncontrolled (defaultChecked)
  • Mixed state with the indeterminate prop, shown with a minus
  • Disabled state
  • Invalid state via aria-invalid
  • Enlarged hit area around the 16px box for easier clicking
  • Works with Label or the Field components for labels, descriptions and groups
  • Keyboard navigation and screen reader support
  • Form integration (name, value, required)

Pass readOnly to show a value that can't be changed. Unlike disabled, a read-only checkbox stays focusable and is announced as read-only (aria-readonly), so screen reader users still hear its state.

Usage

import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'

export function MyCheckbox() {
  return (
    <div className="flex items-center gap-2">
      <Checkbox id="confirm-details" />
      <Label htmlFor="confirm-details">I confirm that the information is correct</Label>
    </div>
  )
}

With Field

Use the Field components for a label and description.

import { Checkbox } from '@/components/ui/checkbox'
import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/ui/field'

export function CheckboxWithDescription() {
  return (
    <Field orientation="horizontal">
      <Checkbox id="digital-mailbox" defaultChecked />
      <FieldContent>
        <FieldLabel htmlFor="digital-mailbox">Send decisions to my digital mailbox</FieldLabel>
        <FieldDescription>
          Decisions and letters arrive digitally instead of by post.
        </FieldDescription>
      </FieldContent>
    </Field>
  )
}

Invalid

<Checkbox id="data-consent" aria-invalid />

Examples

Default

A checkbox with a label.

With Description Text

A checkbox with additional descriptive text using Field, FieldContent, FieldLabel and FieldDescription.

Group

A group of checkboxes inside a FieldSet with a legend.

Select All

A parent checkbox that is indeterminate while only some of the rows are selected.

<Checkbox
  checked={selected.length === cases.length}
  indeterminate={selected.length > 0 && selected.length < cases.length}
  onCheckedChange={(checked) => setSelected(checked ? cases.map((c) => c.id) : [])}
/>

Choice Card

Wrap a horizontal Field in a FieldLabel to make the whole card clickable. The card is highlighted while checked.

Disabled

Checkboxes in their disabled state, both unchecked and checked. Explain why an option isn't available.

Read Only

Shows the value without letting it change, by mouse or keyboard. It stays focusable.

Invalid

A checkbox marked invalid with aria-invalid, with the error in a FieldError.

Form Example

Checkboxes in a form inside a Card.

Accessibility

The Checkbox component follows WAI-ARIA guidelines:

  • Uses native checkbox role
  • Supports keyboard navigation:
    • Space: Toggle checkbox state
    • Tab: Move focus to checkbox
  • Provides proper labeling through aria-label or aria-labelledby
  • Indicates state through aria-checked
  • Communicates disabled state via aria-disabled
  • Maintains focus management
  • Supports form validation

Best Practices

  1. Labeling:

    • Always provide a clear, descriptive label
    • Use concise, action-oriented text
    • Place labels to the right of checkboxes
    • Consider additional description for complex options
  2. Visual Design:

    • Maintain sufficient spacing between checkboxes
    • Ensure adequate touch target size
    • Provide clear visual feedback for all states
    • Use consistent styling across the application
  3. Interaction:

    • Group related checkboxes
    • Use FieldSet and FieldLegend for groups
    • Provide clear error states with aria-invalid
  4. Implementation:

    • Use semantic HTML
    • Implement proper form validation
    • Consider controlled vs. uncontrolled usage
    • Test with keyboard and screen readers
  • Radio Group
  • Switch
  • Toggle
  • Field
  • Form