Use Checkbox for independent choices or explicit agreement. Use Radio Group when exactly one option is allowed and Switch for an on/off setting.
Live preview
import { Checkbox } from "@/components/ui/checkbox";
const options = [
{
id: "component-source",
label: "Component source",
detail: "Include the editable React and TypeScript files.",
checked: true,
},
{
id: "theme-tokens",
label: "Theme tokens",
detail: "Include the matching light and dark CSS variables.",
checked: true,
},
{
id: "private-preview",
label: "Private preview link",
detail: "Available after connecting a deployment provider.",
checked: false,
disabled: true,
},
];
export default function CheckboxDemo() {
return (
<fieldset className="w-full max-w-md rounded-base border-2 border-border bg-secondary-background p-4 shadow-shadow">
<legend className="px-2 font-heading">Release contents</legend>
<div className="divide-y-2 divide-border">
{options.map((option) => (
<label
key={option.id}
htmlFor={option.id}
className={`flex min-h-16 items-start gap-3 py-3 first:pt-1 last:pb-1 ${
option.disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer"
}`}
>
<Checkbox
id={option.id}
defaultChecked={option.checked}
disabled={option.disabled}
className="mt-0.5"
/>
<span>
<strong className="block font-heading">{option.label}</strong>
<span className="mt-1 block text-sm leading-5 text-foreground/75">
{option.detail}
</span>
</span>
</label>
))}
</div>
</fieldset>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/checkbox.jsonnpm install @base-ui/react@^1.8.0Create 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";
function Checkbox({ className, nativeButton, render, ...props }: CheckboxPrimitive.Root.Props) {
const usesDefaultButton = render === undefined;
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
nativeButton={nativeButton ?? usesDefaultButton}
render={render ?? <button type="button" />}
{...props}
className={(state) =>
cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-base border-2 border-input bg-secondary-background outline-hidden transition-colors focus-visible:ring-1 focus-visible:ring-ring data-checked:border-border data-checked:bg-main data-checked:text-main-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 data-indeterminate:border-border data-indeterminate:bg-main data-indeterminate:text-main-foreground",
typeof className === "function" ? className(state) : className,
)
}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
render={(indicatorProps, state) => (
<span {...indicatorProps}>
{state.indeterminate ? (
<MinusIcon data-slot="checkbox-mixed-mark" aria-hidden="true" />
) : (
<CheckIcon data-slot="checkbox-checked-mark" aria-hidden="true" />
)}
</span>
)}
/>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };
#Usage
"use client";
import { useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
export default function ConsentField() {
const [accepted, setAccepted] = useState(false);
return (
<div className="grid gap-3">
<div className="flex items-center gap-2">
<Checkbox
id="terms"
checked={accepted}
onCheckedChange={(value) => setAccepted(value === true)}
/>
<label htmlFor="terms">Accept the terms</label>
</div>
<p role="status">{accepted ? "Accepted in this preview." : "Not accepted."}</p>
</div>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
checked / defaultChecked | Boolean checked state | Unchecked |
onCheckedChange | New boolean checked state and event details | — |
indeterminate | Explicit mixed state, independent of checked | false |
disabled / required | Interaction and form constraints | false |
name / value / form | Form association through a hidden input | — |
id / inputRef | Label association and hidden input reference | — |
#Accessibility
Associate a visible label with the checkbox. Space toggles the focused control. Use mixed state for partial selection, not as a third agreement option. Checked state does not submit or save anything on its own. Treat indeterminate as presentation for partial selection; onCheckedChange still reports a boolean checked state.