docs / core / checkbox
Checkbox
the collecting control. it gathers choices and waits for a submit, ticking one commits nothing until you send the form. that deferral is the whole line between it and Switch.
intensity · recedesrsc · client
provenance
authored in usvalayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
forms, settings rows, filter groups. anywhere choices accumulateindeterminate heads a group whose children disagreenot for actions that apply immediately. that is a switch, not a checkboxnever unlabelled. the label is the click target and the accessible namea11y
role="checkbox" named by its label · disabled sets aria-disabled · the check icon is aria-hiddenjest-axedependencies
@base-ui/react · class-variance-authoritylive demo · try it out
You agree to our terms of service and privacy policy.
customize
labelthe click target and accessible name
descriptionhelper text under the control
sizesm for dense rows
defaultCheckedstarts ticked
indeterminatethe mixed state for a group head
disabledgreys out the whole field
usage
import { Checkbox } from "usva/primitives/checkbox";
<Checkbox
label="Accept terms"
description="You agree to our terms of service and privacy policy."
/>props
| prop | type | default | notes |
|---|---|---|---|
| checked | boolean | — | controlled checked state. |
| defaultChecked | boolean | false | initial checked state when uncontrolled. |
| onCheckedChange | (checked: boolean) => void | — | fires with the next checked state. |
| indeterminate | boolean | false | the mixed state, for a parent whose children disagree. |
| disabled | boolean | false | disables the whole field, label included. |
| label | ReactNode | — | the field label, wired to the control by id. |
| description | ReactNode | — | helper text under the control. |
| size | "sm" | "md" | "md" | sm for dense rows. the hidden hit area grows to compensate. |
get it
npx shadcn add https://usva.build/r/checkbox.jsonsource · components/ui/checkbox.tsxexactly what this command copies
"use client";
import { Checkbox as Base } from "@base-ui/react/checkbox";
import { Field } from "@base-ui/react/field";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const rootVariants = cva(
"relative flex shrink-0 items-center justify-center rounded-md border border-border bg-surface outline-none transition-control duration-base ease-soft before:absolute before:content-[''] data-[unchecked]:hover:border-border-strong data-[checked]:border-accent data-[checked]:bg-accent data-[checked]:bg-gradient-accent data-[checked]:glow-ring data-[indeterminate]:border-accent data-[indeterminate]:bg-accent data-[indeterminate]:bg-gradient-accent data-[indeterminate]:glow-ring active:scale-[0.98] motion-reduce:transition-none motion-reduce:transform-none focus-visible:border-transparent focus-visible:ring-focus aria-invalid:border-danger data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
{
variants: {
size: {
sm: "h-4 w-4 before:-inset-3.5",
md: "h-5 w-5 before:-inset-3",
},
},
defaultVariants: { size: "md" },
},
);
const indicatorVariants = cva("text-on-accent", {
variants: {
size: {
sm: "h-3 w-3",
md: "h-3.5 w-3.5",
},
},
defaultVariants: { size: "md" },
});
const descriptionVariants = cva("text-xs text-muted", {
variants: {
size: {
sm: "pl-6",
md: "pl-7",
},
},
defaultVariants: { size: "md" },
});
export interface CheckboxProps
extends Omit<
React.ComponentPropsWithoutRef<typeof Base.Root>,
"render" | "className"
>,
VariantProps<typeof rootVariants> {
className?: string;
label?: React.ReactNode;
description?: React.ReactNode;
}
export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
({ className, size, label, description, id, disabled, ...props }, ref) => {
const generatedId = React.useId();
const checkboxId = id ?? generatedId;
return (
<Field.Root className="flex flex-col gap-1" disabled={disabled}>
<div className="flex items-center gap-2">
<Base.Root
ref={ref}
id={checkboxId}
disabled={disabled}
className={cn("group", rootVariants({ size }), className)}
{...props}
>
<Base.Indicator
keepMounted
className={cn(
"flex items-center justify-center transition-control duration-base ease-spring motion-reduce:transition-none motion-reduce:transform-none data-[unchecked]:scale-0 data-[checked]:scale-100 data-[indeterminate]:scale-100",
)}
>
<CheckIcon
className={cn(
indicatorVariants({ size }),
"group-data-[indeterminate]:hidden",
)}
/>
<MinusIcon
className={cn(
indicatorVariants({ size }),
"hidden group-data-[indeterminate]:block",
)}
/>
</Base.Indicator>
</Base.Root>
{label ? (
<Field.Label
htmlFor={checkboxId}
className="text-sm text-ink select-none data-[disabled]:opacity-50"
>
{label}
</Field.Label>
) : null}
</div>
{description ? (
<Field.Description className={descriptionVariants({ size })}>
{description}
</Field.Description>
) : null}
</Field.Root>
);
},
);
Checkbox.displayName = "Checkbox";
function CheckIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M2.5 6.5 5 9l4.5-5.5" />
</svg>
);
}
function MinusIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M2.5 6h7" />
</svg>
);
}