Skip to content
ssntl/ui

Checkbox

A native checkbox whose whole label row is the target and never gets selected.

Order alerts

Pick what reaches your phone.

Installation

$ pnpm dlx shadcn@latest add @ssntl/checkbox

Usage

tsx
import { Checkbox, CheckboxLabel } from "@/components/ui/checkbox"

<div role="group" aria-labelledby="alerts-title">
  <p id="alerts-title" className="text-heading">Order alerts</p>
  <CheckboxLabel>
    <Checkbox name="alerts" value="orders" defaultChecked />
    New orders
  </CheckboxLabel>
</div>
  • A native input: it submits with the form and works with the keyboard and screen readers.
  • The label wraps the box, so the whole 48px row toggles it; select-none keeps a double click from selecting the words.
  • Group related boxes under a heading role (text-heading), not body text, tied with aria-labelledby.
  • Links inside a label still work; give them descriptive text.

Code

components/ui/checkbox.tsx
import * as React from "react"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

// A native checkbox, so forms, keyboard and screen readers work as they
// should. The box is 24px; CheckboxLabel makes the whole 48px row the target.
function Checkbox({
  className,
  ...props
}: Omit<React.ComponentProps<"input">, "type">) {
  return (
    <span
      data-slot="checkbox"
      className="inline-grid size-6 shrink-0 place-items-center"
    >
      <input
        type="checkbox"
        className={cn(
          "peer col-start-1 row-start-1 size-6 cursor-pointer appearance-none rounded-xs border border-input bg-card",
          "transition-[background-color,border-color] duration-(--duration-fast) ease-out checked:border-primary checked:bg-primary hover:border-foreground",
          "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
          "disabled:cursor-not-allowed aria-invalid:border-destructive",
          className
        )}
        {...props}
      />
      {/* A heavier stroke than the 1.6 default, so the mark reads at 16px. */}
      <Check
        aria-hidden
        strokeWidth={2}
        className="pointer-events-none col-start-1 row-start-1 size-4 scale-50 text-primary-foreground opacity-0 transition-[opacity,scale] duration-(--duration-fast) ease-out peer-checked:scale-100 peer-checked:opacity-100 motion-reduce:scale-100"
      />
    </span>
  )
}

// The label wraps the box, so the text toggles it too. select-none keeps a
// quick double click from highlighting the words instead of checking twice.
function CheckboxLabel({ className, ...props }: React.ComponentProps<"label">) {
  return (
    <label
      data-slot="checkbox-label"
      className={cn(
        "flex min-h-12 cursor-pointer items-center gap-3 text-body select-none has-disabled:cursor-not-allowed has-disabled:opacity-40",
        className
      )}
      {...props}
    />
  )
}

export { Checkbox, CheckboxLabel }