Zum Inhalt springen
UI Kit
Accent

Toggle Group für React

Drückbare Knöpfe für einen oder mehrere Werte mit aria-pressed und einem Tab-Stopp. Fünf Formen, Tooltips an Icons, Grenzen für Mehrfachwahl, durchgestrichene Größen und eine Werkzeugleiste über mehrere Gruppen.

5 VariantsEine von 9 Components in Buttons

Drückbare Gruppen für einen oder viele Werte

Eine Umschaltgruppe verbindet drückbare Knöpfe für einen Wert oder für viele. Sie nutzt aria-pressed mit einem einzigen wandernden Tab-Stopp, eine Gruppe aus zehn Knöpfen kostet also ein Tab, und kommt in fünf Looks, verbunden, einzeln, Ghost, rund und als Karten.

Die „Textformat-Leiste“ hält drei Gruppen in einer Werkzeugleiste nach dem APG-Muster, Stile lassen sich kombinieren, Ausrichtung nimmt einen Wert, Listen lassen sich abwählen, Tooltips nennen jedes Icon mit Kürzel. Der „Ansichtsumschalter“ verbindet Icon-Segmente, die eine Projektliste zwischen Liste, Galerie, Board und Kalender schalten. „Wochentage“ bietet runde Tagesknöpfe für einen wiederkehrenden Plan, beliebig viele, aber mindestens einer, mit Vorgaben und einem Satz, der die Wahl vorliest. „Größen“ nutzt einzelne Knöpfe für die Größe eines Produkts, ausverkaufte bleiben erreichbar und werden als nicht verfügbar vorgelesen. „Senkrechte Karten“ stapelt Kartenebenen als Karten mit Icon, Beschreibung und Haken, bis zu drei bleiben an.

Setz ein Minimum oder Maximum, wo die Wahl es braucht, mindestens ein Wochentag, höchstens drei Ebenen. Lass ausverkaufte Optionen sichtbar und durchgestrichen, Menschen wollen wissen, dass es eine Größe gibt. Gib jedem reinen Icon-Knopf einen Tooltip mit seinem Namen.

Variants

Textformat-LeisteNeu in 1.2

Drei Gruppen in einer Werkzeugleiste nach dem APG-Muster. Stile lassen sich kombinieren, Ausrichtung nimmt einen Wert, Listen lassen sich abwählen. Ein Tab-Stopp, Pfeile laufen über alle Gruppen, Tooltips nennen jedes Icon mit Kürzel.

AnsichtsumschalterNeu in 1.2

Verbundene Icon-Segmente schalten eine Projektliste zwischen Liste, Galerie, Board und Kalender. Jedes Icon nennt sich im Tooltip, der Inhalt darunter wechselt mit.

WochentageNeu in 1.2

Runde Tagesknöpfe für einen wiederkehrenden Plan, beliebig viele, aber mindestens einer. Vorgaben füllen die Woche mit einem Tipp, der Satz darunter liest die Wahl vor.

GrößenNeu in 1.2

Einzelne Knöpfe für die Größe eines Produkts. Ausverkaufte Größen bleiben erreichbar und werden als nicht verfügbar vorgelesen, ein kleiner Hinweis meldet die letzten Stücke. Darunter eine verbundene Gruppe für die Passform.

Senkrechte KartenNeu in 1.2

Kartenebenen als senkrechte Gruppe aus Karten mit Icon, Beschreibung und Haken. Pfeil hoch und runter bewegen, bis zu drei bleiben an, die Karte daneben folgt jeder Änderung.

Details

Was Umschaltgruppe mitbringt, aus Registry und Quellcode gelesen.

Kategorie
Buttons
Bereich
Elements
Variants
5
Version
1.0.0 · seit 1.2.0
Datei
components/ui/toggle-group.tsx
Bringt mit
utilskiticontooltip
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 4 eigene Texte
Studio
Im Code anpassen

Props

Die Schnittstelle ToggleGroupProps aus components/ui/toggle-group.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • itemsPflicht
    ToggleGroupItem[]
  • typeoptional
    'single' | 'multiple'

    single: one value like a radio set. multiple: any number like checkboxes.

  • valueoptional
    string[]
  • defaultValueoptional
    string[]
  • onValueChangeoptional
    (value: string[]) => void
  • labelPflicht
    string

    Accessible name of the group

  • showLabeloptional
    boolean

    Show the label above the group

  • lookoptional
    ToggleGroupLook

    attachedseparateghostroundcards

  • sizeoptional
    ToggleGroupSize

    smmdlg

  • orientationoptional
    'horizontal' | 'vertical'
  • allowEmptyoptional
    boolean

    single: the chosen item can be pressed again to clear it

  • minoptional
    number

    multiple: fewest and most chosen values

  • maxoptional
    number
  • tooltipsoptional
    boolean

    Tooltips on icon-only items, on by default

  • fullWidthoptional
    boolean

    Items share the full width

Eingebaute Texte

Diese Texte schreibt Umschaltgruppe selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 4 Texte anzeigen
atMostEnglisch Up to {n} can be chosenDeutsch Bis zu {n} sind wählbar
atLeastEnglisch At least {n} stays chosenDeutsch Mindestens {n} bleibt gewählt
unavailableEnglisch unavailableDeutsch nicht verfügbar
selectedEnglisch {n} chosenDeutsch {n} gewählt

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Umschaltgruppe reagiert selbst auf die Tasten Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts, Pos1, Ende und Leertaste.
  • Setzt aria-describedby, aria-disabled, aria-label, aria-labelledby, aria-live, aria-orientation und aria-pressed, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen group, separator und toolbar.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • 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, 352 Zeilen
components/ui/toggle-group.tsx
'use client';

import { createContext, useContext, useEffect, useId, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { Tooltip } from '@/components/ui/tooltip';

/*
 * Name note: toggle.tsx also exports a ToggleGroup (and ToggleGroupProps), the simple segmented one.
 * The CLI installs each component into its own file and neither imports the other, so both live
 * side by side in one project. To use both in one file, rename on import:
 *   import { ToggleGroup as RichToggleGroup } from '@/components/ui/toggle-group';
 * This one adds looks, sizes, min and max choices, unavailable items and the toolbar pattern.
 */
const TEXTS = {
  en: { atMost: 'Up to {n} can be chosen', atLeast: 'At least {n} stays chosen', unavailable: 'unavailable', selected: '{n} chosen' },
  de: { atMost: 'Bis zu {n} sind wählbar', atLeast: 'Mindestens {n} bleibt gewählt', unavailable: 'nicht verfügbar', selected: '{n} gewählt' },
};

export interface ToggleGroupItem {
  value: string;
  label: string;
  icon?: IconName;
  /** Only the icon shows, the label becomes the name and the tooltip */
  iconOnly?: boolean;
  /** A fuller name for screen readers when the visible label is short, e.g. Monday for Mo */
  srLabel?: string;
  /** A second line, for the cards look */
  description?: string;
  /** A small count or tag after the label, e.g. 24 */
  badge?: string;
  /** Keyboard shortcut shown in the tooltip, e.g. ["⌘", "B"] */
  shortcut?: string[];
  disabled?: boolean;
  /** Crossed out and not choosable but still reachable, e.g. a sold out size */
  unavailable?: boolean;
}

/**
 * attached: joined segments with shared borders. separate: single outlined buttons (sizes).
 * ghost: quiet toolbar buttons. round: circles (weekdays). cards: larger rows with icon,
 * description and a check (layers, channels).
 */
export type ToggleGroupLook = 'attached' | 'separate' | 'ghost' | 'round' | 'cards';
export type ToggleGroupSize = 'sm' | 'md' | 'lg';

export interface ToggleGroupProps {
  items: ToggleGroupItem[];
  /** single: one value like a radio set. multiple: any number like checkboxes. */
  type?: 'single' | 'multiple';
  value?: string[];
  defaultValue?: string[];
  onValueChange?: (value: string[]) => void;
  /** Accessible name of the group */
  label: string;
  /** Show the label above the group */
  showLabel?: boolean;
  look?: ToggleGroupLook;
  size?: ToggleGroupSize;
  orientation?: 'horizontal' | 'vertical';
  /** single: the chosen item can be pressed again to clear it */
  allowEmpty?: boolean;
  /** multiple: fewest and most chosen values */
  min?: number;
  max?: number;
  /** Tooltips on icon-only items, on by default */
  tooltips?: boolean;
  /** Items share the full width */
  fullWidth?: boolean;
  labels?: Partial<(typeof TEXTS)['en']>;
  className?: string;
}

/* Inside a ToggleToolbar the toolbar owns the arrow keys across all its groups */
const ToolbarContext = createContext(false);

const fill = (s: string, n: number) => s.replace('{n}', String(n));

function popIcon(button: HTMLElement) {
  const svg = button.querySelector('svg[data-tg-icon]');
  if (!svg || prefersReducedMotion() || typeof svg.animate !== 'function') return;
  svg.animate([{ transform: 'scale(1)' }, { transform: 'scale(0.72)', offset: 0.25 }, { transform: 'scale(1.16)', offset: 0.65 }, { transform: 'scale(1)' }], { duration: 440, easing: 'cubic-bezier(0.2,0.7,0.2,1)' });
}

function nudge(button: HTMLElement) {
  if (prefersReducedMotion() || typeof button.animate !== 'function') return;
  button.animate([{ transform: 'translateX(0)' }, { transform: 'translateX(-3px)' }, { transform: 'translateX(3px)' }, { transform: 'translateX(-2px)' }, { transform: 'translateX(0)' }], { duration: 300, easing: 'ease-out' });
}

/** Moves focus between the enabled items of a container with arrows, Home and End (roving tabindex) */
function rove(e: React.KeyboardEvent<HTMLElement>, container: HTMLElement | null, orientation: 'horizontal' | 'vertical' | 'both') {
  const next = orientation === 'vertical' ? ['ArrowDown'] : orientation === 'horizontal' ? ['ArrowRight'] : ['ArrowRight', 'ArrowDown'];
  const prev = orientation === 'vertical' ? ['ArrowUp'] : orientation === 'horizontal' ? ['ArrowLeft'] : ['ArrowLeft', 'ArrowUp'];
  if (![...next, ...prev, 'Home', 'End'].includes(e.key) || !container) return;
  const list = Array.from(container.querySelectorAll<HTMLElement>('[data-tg-item]:not(:disabled)'));
  const i = list.indexOf(document.activeElement as HTMLElement);
  if (i < 0) return;
  e.preventDefault();
  const rtl = getComputedStyle(container).direction === 'rtl' && orientation !== 'vertical';
  let step = next.includes(e.key) ? 1 : -1;
  if (rtl && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) step = -step;
  const to = e.key === 'Home' ? 0 : e.key === 'End' ? list.length - 1 : (i + step + list.length) % list.length;
  list.forEach((el, j) => (el.tabIndex = j === to ? 0 : -1));
  list[to].focus();
}

const SIZE: Record<ToggleGroupSize, { h: string; px: string; sq: string; text: string; icon: number; round: string }> = {
  sm: { h: 'h-11 sm:h-9', px: 'px-3', sq: 'w-11 sm:w-9', text: 'text-[13px]', icon: 16, round: 'size-11 sm:size-9 text-[13px]' },
  md: { h: 'h-11', px: 'px-3.5', sq: 'w-11', text: 'text-sm', icon: 18, // Seven days fit one row on a phone: 38 px circles whose hit area reaches 44 px
    round: "size-[38px] text-sm after:absolute after:-inset-[3px] after:content-[''] sm:size-11 sm:after:inset-0" },
  lg: { h: 'h-12', px: 'px-4', sq: 'w-12', text: 'text-[15px]', icon: 19, round: 'size-12 text-[15px]' },
};

const FOCUS = 'outline-none focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-ring';

const ITEM: Record<ToggleGroupLook, string> = {
  attached: cn(
    'text-muted-foreground hover:bg-accent hover:text-foreground',
    'aria-pressed:bg-[color-mix(in_oklab,var(--primary)_14%,var(--background))] aria-pressed:text-foreground aria-pressed:hover:bg-[color-mix(in_oklab,var(--primary)_18%,var(--background))] dark:aria-pressed:bg-[color-mix(in_oklab,var(--primary)_24%,var(--background))]',
  ),
  separate: cn(
    'rounded-lg border border-border-strong bg-background text-foreground shadow-[var(--shadow-sm)] hover:border-[color-mix(in_oklab,var(--foreground)_40%,var(--border-strong))]',
    'aria-pressed:border-primary aria-pressed:bg-[color-mix(in_oklab,var(--primary)_8%,var(--background))] aria-pressed:shadow-[0_0_0_1px_var(--primary),var(--shadow-sm)]',
  ),
  ghost: 'rounded-md text-muted-foreground hover:bg-accent hover:text-foreground aria-pressed:bg-accent aria-pressed:text-foreground',
  round: cn(
    'rounded-full border border-border-strong bg-background font-medium text-muted-foreground hover:border-[color-mix(in_oklab,var(--foreground)_40%,var(--border-strong))] hover:text-foreground',
    'aria-pressed:border-primary aria-pressed:bg-primary aria-pressed:text-primary-foreground aria-pressed:hover:bg-[color-mix(in_oklab,var(--primary)_88%,var(--foreground))]',
  ),
  cards: cn(
    'rounded-xl border border-border bg-card text-left text-foreground shadow-[var(--shadow-sm)] hover:border-border-strong',
    'aria-pressed:border-[color-mix(in_oklab,var(--primary)_55%,var(--border))] aria-pressed:bg-[color-mix(in_oklab,var(--primary)_6%,var(--card))]',
  ),
};

/**
 * A group of pressable buttons for one or many values, each with aria-pressed and one tab stop for the
 * whole group (roving tabindex). Five looks, horizontal or vertical, tooltips on icon-only items, limits
 * for multiple choice, crossed out unavailable items. Several groups form one toolbar in ToggleToolbar.
 *   <ToggleGroup label="Days" type="multiple" look="round" min={1} items={[{ value: 'mon', label: 'Mo' }, …]} />
 */
export function ToggleGroup({
  items,
  type = 'single',
  value: valueProp,
  defaultValue,
  onValueChange,
  label,
  showLabel,
  look = 'attached',
  size = 'md',
  orientation = 'horizontal',
  allowEmpty,
  min = 0,
  max,
  tooltips = true,
  fullWidth,
  labels,
  className,
}: ToggleGroupProps) {
  const t = useTexts(TEXTS, labels);
  const inToolbar = useContext(ToolbarContext);
  const id = useId();
  const ref = useRef<HTMLDivElement>(null);
  const multiple = type === 'multiple';
  const [inner, setInner] = useState<string[]>(defaultValue ?? (multiple || allowEmpty ? [] : items[0] ? [items[0].value] : []));
  const value = valueProp ?? inner;
  const [notice, setNotice] = useState('');
  const vertical = orientation === 'vertical';
  const s = SIZE[size];

  // The tab stop: the first chosen item, else the first that can take focus
  const usable = items.map((it, i) => (it.disabled ? -1 : i)).filter((i) => i >= 0);
  const firstOn = items.findIndex((it) => !it.disabled && value.includes(it.value));
  const [stop, setStop] = useState(firstOn >= 0 ? firstOn : usable[0] ?? 0);

  const press = (it: ToggleGroupItem, el: HTMLElement) => {
    if (it.disabled || it.unavailable) return nudge(el);
    const on = value.includes(it.value);
    let next: string[];
    if (multiple) {
      if (on && value.length <= min) {
        setNotice(fill(t.atLeast, min));
        return nudge(el);
      }
      if (!on && max !== undefined && value.length >= max) {
        setNotice(fill(t.atMost, max));
        return nudge(el);
      }
      next = on ? value.filter((v) => v !== it.value) : items.filter((x) => x.value === it.value || value.includes(x.value)).map((x) => x.value);
    } else {
      if (on && !allowEmpty) return;
      next = on ? [] : [it.value];
    }
    setNotice('');
    if (!on) popIcon(el);
    if (valueProp === undefined) setInner(next);
    onValueChange?.(next);
  };

  useEffect(() => {
    if (!notice) return;
    const timer = window.setTimeout(() => setNotice(''), 2600);
    return () => window.clearTimeout(timer);
  }, [notice]);

  const attached = look === 'attached';
  const cards = look === 'cards';

  const button = (it: ToggleGroupItem, i: number) => {
    const on = value.includes(it.value);
    const iconOnly = Boolean(it.iconOnly && it.icon);
    const el = (
      <button
        key={it.value}
        type="button"
        data-tg-item=""
        aria-pressed={on}
        aria-label={iconOnly ? it.label : it.srLabel}
        aria-disabled={it.unavailable || undefined}
        aria-describedby={it.unavailable ? `${id}-na` : undefined}
        disabled={it.disabled}
        tabIndex={inToolbar ? undefined : i === stop ? 0 : -1}
        onFocus={() => !inToolbar && setStop(i)}
        onClick={(e) => press(it, e.currentTarget)}
        className={cn(
          'group/tg relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap font-medium',
          'transition-[background-color,color,border-color,box-shadow,scale] duration-200 ease-[var(--ease-kit)] active:scale-[0.96] active:duration-100 motion-reduce:active:scale-100',
          'disabled:cursor-not-allowed disabled:opacity-45 aria-disabled:cursor-not-allowed',
          FOCUS,
          ITEM[look],
          look === 'round' ? s.round : cards ? 'min-h-14 w-full justify-start gap-3 px-3.5 py-3 text-sm' : cn(s.h, s.text, iconOnly ? cn(s.sq, 'px-0') : s.px),
          fullWidth && !cards && 'flex-1',
          attached && (vertical ? 'w-full justify-start' : ''),
          it.unavailable && 'text-subtle-foreground hover:text-subtle-foreground',
        )}
      >
        {it.icon && (
          <span className={cn(cards && 'grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground transition-colors duration-200 group-aria-pressed/tg:bg-[color-mix(in_oklab,var(--primary)_14%,var(--card))] group-aria-pressed/tg:text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]')}>
            <Icon data-tg-icon="" name={it.icon} size={cards ? 18 : s.icon} strokeWidth={on ? 1.9 : 1.6} />
          </span>
        )}
        {!iconOnly &&
          (cards ? (
            <span className="min-w-0 flex-1">
              <span className="block truncate font-medium text-foreground">{it.label}</span>
              {it.description && <span className="mt-0.5 block whitespace-normal text-[13px] font-normal leading-snug text-muted-foreground">{it.description}</span>}
            </span>
          ) : (
            <span className={cn(it.unavailable && 'line-through decoration-[1.5px]')}>{it.label}</span>
          ))}
        {it.badge && !cards && (
          <span className={cn('rounded-[5px] px-1.5 py-px text-[11px] font-medium tabular-nums', look === 'round' ? 'hidden' : 'bg-muted text-muted-foreground group-aria-pressed/tg:bg-[color-mix(in_oklab,var(--primary)_16%,var(--background))] group-aria-pressed/tg:text-foreground')}>
            {it.badge}
          </span>
        )}
        {cards && (
          <span aria-hidden className="grid size-5 shrink-0 place-items-center rounded-full border border-border-strong transition-colors duration-200 group-aria-pressed/tg:border-primary group-aria-pressed/tg:bg-primary group-aria-pressed/tg:text-primary-foreground">
            <Icon name="check" size={13} strokeWidth={2.4} className="scale-50 opacity-0 transition-[scale,opacity] duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] group-aria-pressed/tg:scale-100 group-aria-pressed/tg:opacity-100 motion-reduce:transition-none" />
          </span>
        )}
        {it.unavailable && <span className="sr-only">, {t.unavailable}</span>}
      </button>
    );
    return iconOnly && tooltips ? (
      <Tooltip key={it.value} content={it.label} keys={it.shortcut} side={vertical ? 'right' : 'top'}>
        {el}
      </Tooltip>
    ) : (
      el
    );
  };

  return (
    <div className={cn('grid min-w-0 gap-2.5', fullWidth && 'w-full', className)}>
      {showLabel && (
        <span id={`${id}-l`} className="text-sm font-medium text-foreground">
          {label}
        </span>
      )}
      <div
        ref={ref}
        role="group"
        aria-label={showLabel ? undefined : label}
        aria-labelledby={showLabel ? `${id}-l` : undefined}
        onKeyDown={inToolbar ? undefined : (e) => rove(e, ref.current, vertical ? 'vertical' : 'horizontal')}
        className={cn(
          'relative min-w-0',
          cards ? 'grid grid-cols-[minmax(0,1fr)] gap-2' : 'flex',
          !cards && (vertical ? 'flex-col items-stretch' : 'flex-wrap items-center'),
          fullWidth || cards ? 'w-full' : 'w-fit max-w-full',
          attached &&
            cn(
              'flex-nowrap overflow-hidden rounded-lg border border-border-strong bg-background shadow-[var(--shadow-sm)]',
              // Hairlines between the segments as shadows on the items, the frame clips the first one
              vertical ? '[&_[data-tg-item]]:shadow-[0_-1px_0_var(--border)]' : '[&_[data-tg-item]]:shadow-[-1px_0_0_var(--border)]',
              '[&>*]:rounded-none',
            ),
          look === 'separate' && 'gap-2',
          look === 'ghost' && 'gap-0.5',
          look === 'round' && 'gap-0.5 sm:gap-2',
        )}
      >
        {items.map(button)}
      </div>
      <span id={`${id}-na`} hidden>
        {t.unavailable}
      </span>
      <p aria-live="polite" className={cn('min-h-0 text-[13px] text-muted-foreground transition-opacity duration-300', notice ? 'opacity-100' : 'sr-only')}>
        {notice}
      </p>
    </div>
  );
}

/**
 * A toolbar after the APG pattern that holds several groups: one tab stop for all of them,
 * arrow keys walk across every item, Home and End jump to the ends.
 *   <ToggleToolbar label="Formatting"><ToggleGroup … /><ToolbarSeparator /><ToggleGroup … /></ToggleToolbar>
 */
export function ToggleToolbar({ label, orientation = 'horizontal', className, children }: { label: string; orientation?: 'horizontal' | 'vertical'; className?: string; children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  // One tab stop: the first pressed item, else the first item. Set after mount, so without JavaScript every button stays reachable.
  useEffect(() => {
    const list = Array.from(ref.current?.querySelectorAll<HTMLElement>('[data-tg-item]:not(:disabled)') ?? []);
    const start = Math.max(0, list.findIndex((el) => el.getAttribute('aria-pressed') === 'true'));
    list.forEach((el, i) => (el.tabIndex = i === start ? 0 : -1));
  }, []);
  return (
    <ToolbarContext.Provider value>
      <div
        ref={ref}
        role="toolbar"
        aria-label={label}
        aria-orientation={orientation}
        onKeyDown={(e) => rove(e, ref.current, orientation)}
        onFocus={(e) => {
          const list = Array.from(ref.current?.querySelectorAll<HTMLElement>('[data-tg-item]') ?? []);
          if (list.includes(e.target as HTMLElement)) list.forEach((el) => (el.tabIndex = el === e.target ? 0 : -1));
        }}
        className={cn('flex min-w-0 items-center gap-1', orientation === 'vertical' ? 'flex-col' : 'flex-wrap', className)}
      >
        {children}
      </div>
    </ToolbarContext.Provider>
  );
}

/** A thin line between the groups of a toolbar */
export function ToolbarSeparator({ orientation = 'vertical', className }: { orientation?: 'horizontal' | 'vertical'; className?: string }) {
  return <span role="separator" aria-orientation={orientation} className={cn('shrink-0 bg-border', orientation === 'vertical' ? 'mx-1 h-6 w-px' : 'my-1 h-px w-6', className)} />;
}

Häufige Fragen

Wie installiere ich Umschaltgruppe?

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

Welche Varianten hat Umschaltgruppe?

5 Varianten, Textformat-Leiste, Ansichtsumschalter, Wochentage, Größen und Senkrechte Karten.

Kann ich die Texte von Umschaltgruppe übersetzen?

Ja. Umschaltgruppe bringt 4 eigene Texte auf Deutsch und Englisch mit, etwa „nicht verfügbar“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Umschaltgruppe einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Umschaltgruppe mit aiw_components und installieren sie mit aiw_add @ai-website/toggle-group.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert