Kontrollkästchen
Echte Checkboxen im Kit-Look: Label, Beschreibung, gemischter Zustand, Gruppen, Karten und Fehler als Text.
Eine von 9 Components in Forms und Inputs
Details
Was Kontrollkästchen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Forms und Inputs
- Bereich
- Elements
- Variants
- 2
- Version
- 1.0.1 · seit 1.0.0
- Datei
- components/ui/checkbox.tsx
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle CheckboxProps aus components/ui/checkbox.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> an.
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>
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-checked, aria-describedby und aria-invalid, damit Screenreader Zweck und Zustand ansagen.
- Jedes Feld hat ein Label, sichtbar oder für Screenreader.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 2 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 161 Zeilen
'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>
);
}Häufige Fragen
Wie installiere ich Kontrollkästchen?
Mit aiw add @ai-website/checkbox. Der Befehl legt die Datei components/ui/checkbox.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon und input. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Kontrollkästchen?
2 Varianten, Zustände und Gruppen und Karten.
Kann ein KI-Assistent Kontrollkästchen einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Kontrollkästchen mit aiw_components und installieren sie mit aiw_add @ai-website/checkbox. Nenn im Prompt die Variante, etwa variant="card", dann schreibt der Assistent deine Inhalte direkt in die Props.