Zum Inhalt springen
UI Kit
Accent

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

2 Variants

Variants

Zustände und Gruppen

Der Haken zeichnet sich, das Kästchen federt. „Alle wählen“ wird gemischt, wenn einige Zeilen an sind, eine Gruppe teilt sich Legende und Hinweis.

Karten

Die ganze Option ist die Trefferfläche. Gewählte Karten bekommen eine weiche Akzentkante.

Details

Was Kontrollkästchen mitbringt, aus Registry und Quellcode gelesen.

Bereich
Elements
Variants
2
Version
1.0.1 · seit 1.0.0
Datei
components/ui/checkbox.tsx
Bringt mit
utilskiticoninput
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

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.

  • labeloptional
    React.ReactNode
  • descriptionoptional
    React.ReactNode
  • indeterminateoptional
    boolean

    Neither checked nor unchecked, e.g. "select all" with some rows selected

  • erroroptional
    React.ReactNode

    Error as text under the label

  • variantoptional
    'default' | 'card'

    card: the whole option is a bordered surface that lights up when checked

  • iconoptional
    IconName
  • refoptional
    React.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
components/ui/checkbox.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert