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/checkboxUsage
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
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 }