Checkbox
React Aria checkbox with animated tick and indeterminate state.
States
Checked, indeterminate, and disabled.
function CheckboxStates() {
return (
<div className="flex flex-wrap items-center gap-6">
<Checkbox>Unchecked</Checkbox>
<Checkbox defaultSelected>Checked</Checkbox>
<Checkbox isIndeterminate>Indeterminate</Checkbox>
<Checkbox isDisabled>Disabled</Checkbox>
<Checkbox defaultSelected isDisabled>
Checked disabled
</Checkbox>
</div>
);
}function CheckboxStates() {
return (
<div className="flex flex-wrap items-center gap-6">
<Checkbox>Unchecked</Checkbox>
<Checkbox defaultSelected>Checked</Checkbox>
<Checkbox isIndeterminate>Indeterminate</Checkbox>
<Checkbox isDisabled>Disabled</Checkbox>
<Checkbox defaultSelected isDisabled>
Checked disabled
</Checkbox>
</div>
);
}Sizes
Small and medium.
function CheckboxSizes() {
return (
<div className="flex flex-wrap items-center gap-6">
<Checkbox size="sm" defaultSelected>
Small
</Checkbox>
<Checkbox size="md" defaultSelected>
Medium
</Checkbox>
</div>
);
}function CheckboxSizes() {
return (
<div className="flex flex-wrap items-center gap-6">
<Checkbox size="sm" defaultSelected>
Small
</Checkbox>
<Checkbox size="md" defaultSelected>
Medium
</Checkbox>
</div>
);
}Installation
npx shadcn@latest add https://boardcn.dev/r/checkbox.jsonnpx shadcn@latest add https://boardcn.dev/r/checkbox.jsonnpm packages
- react-aria-components
BoardCN dependencies
The CLI installs these for you — you do not need to add them yourself.
Source
The 2 files the CLI copies into your project.
import { cx } from "@/utils/cx";
export type CheckboxSize = "sm" | "md";
export const checkboxSizes: Record<
CheckboxSize,
{ box: string; glyph: string; label: string; gap: string }
> = {
md: { box: "size-4", glyph: "size-4", label: "text-body-medium", gap: "gap-2" },
sm: { box: "size-3.5", glyph: "size-3.5", label: "text-body-2-medium", gap: "gap-1.5" },
};
export interface CheckboxGlyphState {
isSelected: boolean;
isIndeterminate: boolean;
isFocusVisible: boolean;
isDisabled: boolean;
isHovered: boolean;
}
/**
* The 16px (or 14px) checkbox box + tick/indeterminate glyph. Shared by the
* standalone Checkbox and the CheckboxCard so their visuals stay identical.
*/
export function CheckboxGlyph({
state,
size = "md",
}: {
state: CheckboxGlyphState;
size?: CheckboxSize;
}) {
const { isSelected, isIndeterminate, isFocusVisible, isDisabled, isHovered } = state;
const s = checkboxSizes[size];
const isMarked = isSelected || isIndeterminate;
const hover = isHovered && !isDisabled;
return (
<span
aria-hidden
className={cx(
"flex shrink-0 items-center justify-center rounded-sm",
"transition-[background-color,border-color,box-shadow] duration-150 ease",
s.box,
isMarked
? cx(
"bg-linear-to-b shadow-checkbox-selected",
hover ? "from-accent-400 to-accent-500" : "from-accent-500 to-accent-600",
)
: cx(
"border bg-background-primary-default shadow-xs",
hover ? "border-border-checkbox-hover" : "border-border-checkbox-default",
),
isDisabled && "opacity-50",
isFocusVisible && "ring-2 ring-border-focus-ring ring-offset-2",
)}
>
<svg viewBox="0 0 16 16" fill="none" className={s.glyph}>
{isIndeterminate ? (
<path d="M4.5 8H8H11.5" stroke="white" strokeWidth="2" strokeLinecap="round" />
) : isSelected ? (
<path
d="M4 7.7002L6.64645 10.3466C6.84171 10.5419 7.15829 10.5419 7.35355 10.3466L12 5.7002"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
pathLength={1}
className="animate-check-draw"
/>
) : null}
</svg>
</span>
);
}import { cx } from "@/utils/cx";
export type CheckboxSize = "sm" | "md";
export const checkboxSizes: Record<
CheckboxSize,
{ box: string; glyph: string; label: string; gap: string }
> = {
md: { box: "size-4", glyph: "size-4", label: "text-body-medium", gap: "gap-2" },
sm: { box: "size-3.5", glyph: "size-3.5", label: "text-body-2-medium", gap: "gap-1.5" },
};
export interface CheckboxGlyphState {
isSelected: boolean;
isIndeterminate: boolean;
isFocusVisible: boolean;
isDisabled: boolean;
isHovered: boolean;
}
/**
* The 16px (or 14px) checkbox box + tick/indeterminate glyph. Shared by the
* standalone Checkbox and the CheckboxCard so their visuals stay identical.
*/
export function CheckboxGlyph({
state,
size = "md",
}: {
state: CheckboxGlyphState;
size?: CheckboxSize;
}) {
const { isSelected, isIndeterminate, isFocusVisible, isDisabled, isHovered } = state;
const s = checkboxSizes[size];
const isMarked = isSelected || isIndeterminate;
const hover = isHovered && !isDisabled;
return (
<span
aria-hidden
className={cx(
"flex shrink-0 items-center justify-center rounded-sm",
"transition-[background-color,border-color,box-shadow] duration-150 ease",
s.box,
isMarked
? cx(
"bg-linear-to-b shadow-checkbox-selected",
hover ? "from-accent-400 to-accent-500" : "from-accent-500 to-accent-600",
)
: cx(
"border bg-background-primary-default shadow-xs",
hover ? "border-border-checkbox-hover" : "border-border-checkbox-default",
),
isDisabled && "opacity-50",
isFocusVisible && "ring-2 ring-border-focus-ring ring-offset-2",
)}
>
<svg viewBox="0 0 16 16" fill="none" className={s.glyph}>
{isIndeterminate ? (
<path d="M4.5 8H8H11.5" stroke="white" strokeWidth="2" strokeLinecap="round" />
) : isSelected ? (
<path
d="M4 7.7002L6.64645 10.3466C6.84171 10.5419 7.15829 10.5419 7.35355 10.3466L12 5.7002"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
pathLength={1}
className="animate-check-draw"
/>
) : null}
</svg>
</span>
);
}"use client";
import type { ReactNode, Ref } from "react";
import { Checkbox as AriaCheckbox } from "react-aria-components";
import type { CheckboxProps as AriaCheckboxProps } from "react-aria-components";
import { cx } from "@/utils/cx";
import { CheckboxGlyph, checkboxSizes } from "./checkbox-glyph";
import type { CheckboxSize } from "./checkbox-glyph";
export interface CheckboxProps extends Omit<AriaCheckboxProps, "children"> {
children?: ReactNode;
size?: CheckboxSize;
ref?: Ref<HTMLLabelElement>;
}
export function Checkbox({ className, children, size = "md", ref, ...props }: CheckboxProps) {
const s = checkboxSizes[size];
return (
<AriaCheckbox
ref={ref}
{...props}
className={(state) =>
cx(
"group inline-flex items-center select-none",
s.gap,
state.isDisabled ? "cursor-not-allowed" : "cursor-pointer",
typeof className === "function" ? className(state) : className,
)
}
>
{(state) => (
<>
<CheckboxGlyph state={state} size={size} />
{children !== undefined && children !== null && (
<span className={cx(s.label, "text-text-primary")}>{children}</span>
)}
</>
)}
</AriaCheckbox>
);
}"use client";
import type { ReactNode, Ref } from "react";
import { Checkbox as AriaCheckbox } from "react-aria-components";
import type { CheckboxProps as AriaCheckboxProps } from "react-aria-components";
import { cx } from "@/utils/cx";
import { CheckboxGlyph, checkboxSizes } from "./checkbox-glyph";
import type { CheckboxSize } from "./checkbox-glyph";
export interface CheckboxProps extends Omit<AriaCheckboxProps, "children"> {
children?: ReactNode;
size?: CheckboxSize;
ref?: Ref<HTMLLabelElement>;
}
export function Checkbox({ className, children, size = "md", ref, ...props }: CheckboxProps) {
const s = checkboxSizes[size];
return (
<AriaCheckbox
ref={ref}
{...props}
className={(state) =>
cx(
"group inline-flex items-center select-none",
s.gap,
state.isDisabled ? "cursor-not-allowed" : "cursor-pointer",
typeof className === "function" ? className(state) : className,
)
}
>
{(state) => (
<>
<CheckboxGlyph state={state} size={size} />
{children !== undefined && children !== null && (
<span className={cx(s.label, "text-text-primary")}>{children}</span>
)}
</>
)}
</AriaCheckbox>
);
}Props
Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.
Checkbox
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | md | — |
CheckboxGlyph
The 16px (or 14px) checkbox box + tick/indeterminate glyph. Shared by the standalone Checkbox and the CheckboxCard so their visuals stay identical.
| Prop | Type | Default | Description |
|---|---|---|---|
| staterequired | CheckboxGlyphState | — | — |
| size | "sm" | "md" | md | — |