Zum Inhalt springen
UI Kit
Accent

Switch für React

An und aus für Einstellungen, die sofort gelten, mit role="switch", als Liste oder als Zeilen.

Eine von 9 Components in Forms und Inputs

2 Variants

Ein und aus, sofort

Ein Switch schaltet eine Einstellung mit sofortiger Wirkung ein oder aus, wie ein Lichtschalter. Dieser ist ein echter Button mit role switch und aria-checked, Screenreader sagen ihn als ein oder aus an, die Leertaste schaltet, und das Label gehört zur Klickfläche.

Die Einstellungsliste stapelt Switches mit je einem Titel und einem Satz, das Layout einer Benachrichtigungsseite. „Zeilen mit Icons“ ergänzt ein Icon je Zeile und passt zu App-Einstellungen wie Dark Mode, Töne und Automatisch speichern. Beide zeigen den Zustand mit Position und Farbe, der Knopf bewegt sich ruhig, bei reduzierter Bewegung gar nicht.

Nutze einen Switch nur, wenn die Änderung sofort gilt. In einem Formular, das mit einem Button gespeichert wird, nimm eine Checkbox. Benenne die Einstellung, nicht den Zustand, E-Mail-Benachrichtigungen statt E-Mail einschalten, der Switch zeigt selbst, ob er an ist.

Variants

Einstellungsliste

Label und Beschreibung links, der Schalter rechts, wie man es vom Telefon kennt. Der Knopf federt hinüber und zeichnet einen kleinen Haken.

Zeilen mit Icons

Jede Einstellung auf eigener Fläche, mit einem Icon, das das Thema benennt.

Details

Was Schalter mitbringt, aus Registry und Quellcode gelesen.

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

Props

Die Schnittstelle SwitchProps aus components/ui/switch.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'value'> an.

  • checkedoptional
    boolean
  • defaultCheckedoptional
    boolean
  • onCheckedChangeoptional
    (checked: boolean) => void
  • labeloptional
    React.ReactNode
  • descriptionoptional
    React.ReactNode
  • sizeoptional
    'sm' | 'md'
  • positionoptional
    'start' | 'end'

    Where the switch sits next to the label. end is the settings-list look.

  • variantoptional
    'default' | 'row'

    row: a bordered settings row, optionally with an icon

  • iconoptional
    IconName
  • nameoptional
    string

    Form name, a hidden input carries "on" when checked

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-checked, aria-describedby und aria-labelledby, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rolle switch.
  • 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, 138 Zeilen
components/ui/switch.tsx
'use client';

import { useId, useState } from 'react';
import { cn } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';

export interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'value'> {
  checked?: boolean;
  defaultChecked?: boolean;
  onCheckedChange?: (checked: boolean) => void;
  label?: React.ReactNode;
  description?: React.ReactNode;
  size?: 'sm' | 'md';
  /** Where the switch sits next to the label. end is the settings-list look. */
  position?: 'start' | 'end';
  /** row: a bordered settings row, optionally with an icon */
  variant?: 'default' | 'row';
  icon?: IconName;
  /** Form name, a hidden input carries "on" when checked */
  name?: string;
  className?: string;
}

const SIZE = {
  sm: { track: 'h-5 w-9', thumb: 'size-4', on: 'translate-x-4', mark: 8 },
  md: { track: 'h-6 w-11', thumb: 'size-5', on: 'translate-x-5', mark: 10 },
};

/**
 * An on/off setting that applies right away. A button with role="switch" and aria-checked.
 *   <Switch label="Email digest" description="A summary every Monday" defaultChecked />
 */
export function Switch({ checked, defaultChecked, onCheckedChange, label, description, size = 'md', position = 'end', variant = 'default', icon, name, className, id, disabled, onClick, ...rest }: SwitchProps) {
  const auto = useId();
  const sid = id ?? auto;
  const [inner, setInner] = useState(defaultChecked ?? false);
  const on = checked ?? inner;
  const s = SIZE[size];

  const control = (
    <button
      type="button"
      role="switch"
      id={sid}
      aria-checked={on}
      aria-labelledby={label ? `${sid}-label` : undefined}
      aria-describedby={description ? `${sid}-desc` : undefined}
      disabled={disabled}
      {...rest}
      onClick={(e) => {
        onClick?.(e);
        setInner(!on);
        onCheckedChange?.(!on);
      }}
      className={cn(
        'group/switch relative inline-flex shrink-0 cursor-pointer items-center rounded-full p-0.5 outline-none transition-[background-color,box-shadow] duration-200 ease-[var(--ease-kit)]',
        'bg-[color-mix(in_oklab,var(--foreground)_14%,var(--background))] shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_6%,transparent)] hover:bg-[color-mix(in_oklab,var(--foreground)_20%,var(--background))]',
        'aria-checked:bg-primary aria-checked:hover:bg-[color-mix(in_oklab,var(--primary)_88%,var(--background))]',
        'focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_25%,transparent)]',
        'disabled:cursor-not-allowed disabled:opacity-50',
        // 44 px hit area
        'after:absolute after:-inset-x-1 after:-inset-y-3 after:content-[""]',
        s.track,
        !label && className,
      )}
    >
      {/* The knob travels on a spring: it overshoots a hair and settles. Held down it stretches. A small check draws itself on it. */}
      <span
        aria-hidden
        className={cn(
          'pointer-events-none grid place-items-center rounded-full shadow-[0_1px_3px_rgb(0_0_0/0.2),0_0_0_0.5px_rgb(0_0_0/0.04)] transition-[translate,scale,background-color] duration-[420ms] ease-[cubic-bezier(0.34,1.45,0.64,1)] group-active/switch:scale-x-[1.18] group-active/switch:duration-150 motion-reduce:transition-none',
          s.thumb,
          on ? cn(s.on, 'origin-right bg-primary-foreground') : 'origin-left bg-background dark:bg-[color-mix(in_oklab,var(--foreground)_88%,var(--background))]',
        )}
      >
        <svg viewBox="0 0 24 24" width={s.mark} height={s.mark} fill="none" className="text-primary">
          <path
            d="m5 12.5 4.5 4.5L19 7.5"
            pathLength={1}
            strokeDasharray="1 1"
            stroke="currentColor"
            strokeWidth="3.4"
            strokeLinecap="round"
            strokeLinejoin="round"
            className={cn('transition-[stroke-dashoffset,opacity] ease-[var(--ease-kit)] motion-reduce:transition-none', on ? 'opacity-100 delay-150 duration-300 [stroke-dashoffset:0]' : 'opacity-0 duration-150 [stroke-dashoffset:1]')}
          />
        </svg>
      </span>
    </button>
  );

  const hidden = name ? <input type="hidden" name={name} value={on ? 'on' : ''} /> : null;
  if (!label) return (
    <>
      {control}
      {hidden}
    </>
  );

  const text = (
    <span className="min-w-0 flex-1">
      <label id={`${sid}-label`} htmlFor={sid} className={cn('block cursor-pointer text-sm font-medium leading-5 text-foreground', disabled && 'cursor-not-allowed')}>
        {label}
      </label>
      {description && (
        <span id={`${sid}-desc`} className="mt-0.5 block text-[13px] leading-5 text-muted-foreground">
          {description}
        </span>
      )}
    </span>
  );

  return (
    <div
      className={cn(
        'flex items-start gap-3.5',
        variant === 'row' && 'items-center rounded-xl border border-border bg-card px-4 py-3.5 shadow-[var(--shadow-sm)]',
        disabled && 'opacity-60',
        className,
      )}
    >
      {variant === 'row' && icon && (
        <span
          className={cn(
            'grid size-9 shrink-0 place-items-center rounded-lg border bg-surface transition-[color,border-color,background-color] duration-300',
            on ? 'border-[color-mix(in_oklab,var(--primary)_35%,var(--border))] bg-[color-mix(in_oklab,var(--primary)_8%,var(--surface))] text-[color-mix(in_oklab,var(--primary)_85%,var(--foreground))]' : 'border-border text-muted-foreground',
          )}
        >
          <Icon name={icon} size={18} />
        </span>
      )}
      {position === 'start' && <span className={cn('flex items-center', size === 'md' ? 'h-6 -mt-0.5' : 'h-5')}>{control}</span>}
      {text}
      {position === 'end' && <span className={cn('flex items-center', variant === 'row' ? '' : size === 'md' ? 'h-6 -mt-0.5' : 'h-5')}>{control}</span>}
      {hidden}
    </div>
  );
}

Häufige Fragen

Wie installiere ich Schalter?

Mit aiw add @ai-website/switch. Der Befehl legt die Datei components/ui/switch.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Schalter?

2 Varianten, Einstellungsliste und Zeilen mit Icons.

Kann ein KI-Assistent Schalter einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Schalter mit aiw_components und installieren sie mit aiw_add @ai-website/switch. Nenn im Prompt die Variante, etwa variant="row", dann schreibt der Assistent deine Inhalte direkt in die Props.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert