AppWindowTabs

ui navigationshellapp-window

Untere Icon-Tab-Leiste für den Bereichswechsel. Generisch über die Id — kennt weder einen Store noch einen Router: `activeId` und `onSelect` verdrahtet die App. Einträge können deaktiviert sein.

Abhängigkeiten

Barrierefreiheit

  • <nav> mit aria-label (Default „Bereich"), überschreibbar.
  • Der aktive Eintrag trägt aria-pressed="true" — es sind Umschalter, keine Links, deshalb aria-pressed statt aria-current.
  • Jeder Eintrag zeigt Label UND Icon; das Label ist sichtbarer Text, nicht nur ein aria-label.

Beispiele

Bereichswechsel
const TABS = [
  { id: 'event', label: 'Event', icon: <EventIcon size={20} /> },
  { id: 'songs', label: 'Songs', icon: <SongIcon size={20} /> },
];

<AppWindowTabs items={TABS} activeId={section} onSelect={setSection} />

Genutzte Tokens · 10

--accent--accent-fg--ease-standard--fg--focus-ring--font-body--motion-fast--muted--space-1--space-4

Quelldateien

components/ui/AppWindowTabs.tsx tsx
import type { ReactNode } from 'react';
import styles from './AppWindowTabs.module.css';

/**
 * Untere Icon-Tab-Leiste des App-Fensters (aus media-presenter
 * `features/sectionnav/SectionNav.tsx`).
 *
 * Bewusst generisch über `Id`: die Leiste kennt weder presenters `Section`-Typ
 * noch einen Router. Welcher Eintrag aktiv ist und was ein Klick auslöst,
 * entscheidet die App (`activeId` / `onSelect`) — Store oder Routing bleiben
 * app-seitig.
 */
export interface AppWindowTab<Id extends string = string> {
  id: Id;
  label: string;
  /** Fertig gerendertes Icon-Element, z. B. `<EventIcon size={20} />`.
   *  ReactNode statt Komponententyp, damit kein Icon-Typ in den Vertrag muss. */
  icon: ReactNode;
  disabled?: boolean;
}

export interface AppWindowTabsProps<Id extends string = string> {
  items: readonly AppWindowTab<Id>[];
  activeId: Id;
  onSelect: (id: Id) => void;
  /** Beschriftung der Navigation für Screenreader. */
  ariaLabel?: string;
  className?: string;
}

export function AppWindowTabs<Id extends string = string>({
  items,
  activeId,
  onSelect,
  ariaLabel = 'Bereich',
  className,
}: AppWindowTabsProps<Id>) {
  const cls = [styles.nav, className].filter(Boolean).join(' ');
  return (
    <nav className={cls} aria-label={ariaLabel}>
      {items.map((eintrag) => {
        const aktiv = eintrag.id === activeId;
        return (
          <button
            key={eintrag.id}
            type="button"
            className={`${styles.button} ${aktiv ? styles.active : ''}`}
            // aria-pressed statt aria-current: es sind Umschalter, keine Links.
            aria-pressed={aktiv}
            disabled={eintrag.disabled ?? false}
            onClick={() => onSelect(eintrag.id)}
          >
            <span className={styles.icon}>{eintrag.icon}</span>
            <span className={styles.label}>{eintrag.label}</span>
          </button>
        );
      })}
    </nav>
  );
}
components/ui/AppWindowTabs.module.css css
.nav {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: var(--space-1);
  height: 100%;
  padding: 0 var(--space-4);
}

.button {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 84px;
  padding: 6px 14px 8px;
  background: transparent;
  border: none;
  border-top: 2px solid transparent;
  color: var(--muted);
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition:
    color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    background var(--motion-fast) var(--ease-standard);
}

.button:hover:not(:disabled) {
  color: var(--fg);
  background: color-mix(in oklab, var(--fg), transparent 95%);
}

.button:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.active {
  color: var(--accent-fg);
  border-top-color: var(--accent);
}

.icon {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
}

.label {
  line-height: 1.1;
}

Holen

# MCP (Claude Code / Cursor)
get_component({ name: "app-window-tabs" })

# Registry direkt
https://www.ekklesi.tools/design/registry/components/app-window-tabs.json