Checkbox
Real checkboxes in the kit look: label, description, mixed state, groups, cards and errors as text.
One of 9 components in Forms and inputs
Details
What Checkbox brings along, read from the registry and the source.
- Category
- Forms and inputs
- Area
- Elements
- Variants
- 2
- Version
- 1.0.1 · since 1.0.0
- File
- components/ui/checkbox.tsx
- npm packages
- None
- Languages
- Texts come through props
- Studio
- Adjust in code
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The CheckboxProps interface from components/ui/checkbox.tsx, with the comments from the code. Also takes every prop of Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'>.
labeloptionalReact.ReactNodedescriptionoptionalReact.ReactNodeindeterminateoptionalbooleanNeither checked nor unchecked, e.g. "select all" with some rows selected
erroroptionalReact.ReactNodeError as text under the label
variantoptional'default' | 'card'card: the whole option is a bordered surface that lights up when checked
iconoptionalIconNamerefoptionalReact.Ref<HTMLInputElement>
Accessibility
Read from the source of the component.
- Sets aria-checked, aria-describedby and aria-invalid, so screen readers announce purpose and state.
- Every field has a label, visible or for screen readers.
- Shows a visible focus ring when used with the keyboard.
- The server renders the starting state in full, JavaScript only adds the interaction.
Motion
- With reduced motion (prefers-reduced-motion) it stays still. 2 places in the code switch motion off then.
- No animations, it stands still from the start.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 161 lines
'use client';
import { useEffect, useId, useRef } from 'react';
import { useState } from 'react';
import { cn, prefersReducedMotion, useInView } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { FieldError, FieldHint } from '@/components/ui/input';
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
label?: React.ReactNode;
description?: React.ReactNode;
/** Neither checked nor unchecked, e.g. "select all" with some rows selected */
indeterminate?: boolean;
/** Error as text under the label */
error?: React.ReactNode;
/** card: the whole option is a bordered surface that lights up when checked */
variant?: 'default' | 'card';
icon?: IconName;
ref?: React.Ref<HTMLInputElement>;
className?: string;
}
/** A spring on the box: it dips, overshoots a little, settles */
function spring(el: HTMLElement) {
if (prefersReducedMotion() || typeof el.animate !== 'function') return;
el.animate([{ transform: 'scale(1)' }, { transform: 'scale(0.84)', offset: 0.25 }, { transform: 'scale(1.08)', offset: 0.65 }, { transform: 'scale(1)' }], { duration: 420, easing: 'cubic-bezier(0.2,0.7,0.2,1)' });
}
/** The box itself. A real checkbox input, drawn by the kit. The check draws itself, the box springs. */
export function CheckboxBox({ indeterminate, className, ref, onChange, ...rest }: Omit<CheckboxProps, 'label' | 'description' | 'error' | 'variant' | 'icon'>) {
const own = useRef<HTMLInputElement>(null);
useEffect(() => {
if (own.current) own.current.indeterminate = Boolean(indeterminate);
}, [indeterminate]);
return (
<span className={cn('relative grid size-[18px] shrink-0 place-items-center', className)}>
<input
{...rest}
ref={(el) => {
own.current = el;
if (typeof ref === 'function') ref(el);
else if (ref) (ref as React.RefObject<HTMLInputElement | null>).current = el;
}}
type="checkbox"
aria-checked={indeterminate ? 'mixed' : undefined}
onChange={(e) => {
spring(e.currentTarget);
onChange?.(e);
}}
className={cn(
'peer absolute inset-0 m-0 size-full cursor-pointer appearance-none rounded-[5px] border border-border-strong bg-background shadow-[var(--shadow-sm)] outline-none transition-[background-color,border-color,box-shadow] duration-150',
'hover:border-[color-mix(in_oklab,var(--foreground)_35%,var(--border-strong))]',
'checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary',
'focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_22%,transparent)] focus-visible:outline-none',
'aria-invalid:border-destructive aria-invalid:checked:bg-destructive',
'disabled:cursor-not-allowed disabled:opacity-50',
)}
/>
<svg
viewBox="0 0 24 24"
width={14}
height={14}
fill="none"
aria-hidden
className="pointer-events-none relative text-primary-foreground peer-indeterminate:opacity-0 [&_path]:[stroke-dashoffset:1] [&_path]:transition-[stroke-dashoffset] [&_path]:duration-150 peer-checked:[&_path]:[stroke-dashoffset:0] peer-checked:[&_path]:duration-300 peer-checked:[&_path]:delay-75 motion-reduce:[&_path]:transition-none"
>
<path d="m5 12.5 4.5 4.5L19 7.5" pathLength={1} strokeDasharray="1 1" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span aria-hidden className="pointer-events-none absolute h-[2px] w-2 scale-x-0 rounded-full bg-primary-foreground opacity-0 transition-[opacity,scale] duration-200 ease-[var(--ease-kit)] peer-indeterminate:scale-x-100 peer-indeterminate:opacity-100" />
</span>
);
}
/**
* Checkbox with label, description and error. card turns the option into a selectable surface.
* <Checkbox label="Send me the monthly report" description="First Monday of the month" />
*/
export function Checkbox({ label, description, error, variant = 'default', icon, className, id, ...rest }: CheckboxProps) {
const auto = useId();
const cid = id ?? auto;
const descId = description ? `${cid}-desc` : undefined;
const errId = error ? `${cid}-err` : undefined;
const box = <CheckboxBox {...rest} id={cid} aria-describedby={cn(descId, errId) || undefined} aria-invalid={error ? true : rest['aria-invalid']} />;
if (!label) return box;
const card = variant === 'card';
return (
<div className={cn('min-w-0', card && 'flex flex-col', className)}>
<label
htmlFor={cid}
className={cn(
'group/check flex cursor-pointer items-start gap-3',
// Phones: the whole row is a 44 px tap target, 12 px above and below the first line belong to it
!card && 'max-sm:py-3',
card &&
'relative flex-1 rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-sm)] transition-[border-color,background-color,box-shadow,translate] duration-200 hover:-translate-y-px hover:border-border-strong motion-reduce:hover:translate-y-0 has-[:checked]:border-[color-mix(in_oklab,var(--primary)_55%,var(--border))] has-[:checked]:bg-[color-mix(in_oklab,var(--primary)_5%,var(--card))] has-[:checked]:shadow-[0_0_0_1px_color-mix(in_oklab,var(--primary)_55%,var(--border)),var(--shadow-sm)] has-[:focus-visible]:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_22%,transparent)]',
rest.disabled && 'cursor-not-allowed opacity-60',
)}
>
{card && icon && (
<span className="grid size-9 shrink-0 place-items-center rounded-lg border border-border bg-surface text-muted-foreground transition-[color,border-color,scale] duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] group-has-[:checked]/check:scale-105 group-has-[:checked]/check:border-[color-mix(in_oklab,var(--primary)_35%,var(--border))] group-has-[:checked]/check:text-[color-mix(in_oklab,var(--primary)_85%,var(--foreground))]">
<Icon name={icon} size={18} />
</span>
)}
<span className={cn('min-w-0 flex-1', card ? 'order-none' : 'order-2')}>
<span className="block text-sm font-medium leading-5 text-foreground">{label}</span>
{description && (
<span id={descId} className="mt-0.5 block text-[13px] leading-5 text-muted-foreground">
{description}
</span>
)}
</span>
<span className={cn('flex h-5 items-center', card ? 'order-3' : 'order-1')}>{box}</span>
</label>
{error && (
<FieldError id={errId} className="mt-2">
{error}
</FieldError>
)}
</div>
);
}
export interface CheckboxGroupProps {
legend: React.ReactNode;
description?: React.ReactNode;
error?: React.ReactNode;
/** Lay the options out in columns from tablet up */
columns?: 1 | 2 | 3;
className?: string;
children: React.ReactNode;
}
/** A set of checkboxes with a legend, a hint and one error for the whole set. The options rise one after another as the set scrolls in. */
export function CheckboxGroup({ legend, description, error, columns = 1, className, children }: CheckboxGroupProps) {
const id = useId();
const [ref, inView] = useInView<HTMLDivElement>();
const [done, setDone] = useState(false);
useEffect(() => {
if (!inView) return;
const t = window.setTimeout(() => setDone(true), 1300);
return () => window.clearTimeout(t);
}, [inView]);
return (
<fieldset className={cn('min-w-0', className)} aria-describedby={error ? `${id}-err` : description ? `${id}-desc` : undefined} aria-invalid={error ? true : undefined}>
<legend className="text-sm font-medium text-foreground">{legend}</legend>
{description && <FieldHint id={`${id}-desc`} className="mt-1">{description}</FieldHint>}
<div
ref={ref}
style={{ '--kit-step': '70ms' } as React.CSSProperties}
className={cn('kit-stagger grid gap-x-4 gap-y-0 sm:mt-4 sm:gap-4', !inView && 'kit-stagger-wait', done && 'kit-stagger-done', columns === 2 && 'sm:grid-cols-2', columns === 3 && 'sm:grid-cols-2 lg:grid-cols-3')}
>
{children}
</div>
{error && (
<FieldError id={`${id}-err`} className="mt-3">
{error}
</FieldError>
)}
</fieldset>
);
}Frequent questions
How do I install Checkbox?
With aiw add @ai-website/checkbox. The command puts components/ui/checkbox.tsx into your project, the code is yours from then on. It brings along icon and input. It needs no further npm packages.
Which variants does Checkbox have?
2 variants, States and groups and Cards.
Can an AI assistant build with Checkbox?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Checkbox with aiw_components and install it with aiw_add @ai-website/checkbox. Name the variant in your prompt, for example variant="card", and the assistant writes your content straight into the props.