Zum Inhalt
← Zugang & Robustheit

Tabs und Segmente mit Pfeiltasten

Reiter und Segmente sind eine Tab-Station: Pfeile, Pos1 und Ende wechseln innerhalb, nativ per focusgroup, sonst per roving tabindex.

Inhalte, die sofort da sind: Funktions-Kapitel auf Produktseiten, Speisekarte, Preise Monat/Jahr.

Tabs und Segment-Schalter sind für die Tastatur eine einzige Station: Tab springt hinein und wieder hinaus, die Pfeiltasten wechseln innerhalb, Pos1 und Ende springen zum ersten und letzten Eintrag. Seit Chrome 150 erledigt das HTML-Attribut focusgroup das Springen selbst, ohne Script. Für alle anderen Browser hält ein kleines Script den „roving tabindex“: nur der gewählte Tab hat tabindex=0, alle anderen -1. Rollen bleiben Pflicht: role=tablist, tab und tabpanel mit aria-selected und aria-controls. Zwei Arten der Aktivierung: automatisch (Auswahl folgt dem Fokus) oder manuell (Pfeile bewegen nur den Fokus, Enter oder Leertaste wählt). Segmente baut man am besten aus nativen Radios, die können Pfeiltasten von Haus aus.

Wer mit der Tastatur arbeitet, muss nicht durch jeden einzelnen Reiter tabben, um zum Inhalt zu kommen. Das Verhalten entspricht dem, was Screenreader-Nutzer von Desktop-Programmen kennen. Der gleitende Indikator zeigt die Auswahl, der Fokusring zeigt, wo man gerade ist; beides bleibt getrennt sichtbar.

Wann einsetzen

Funktions-Kapitel auf Produktseiten, Speisekarte mit Mittag, Abend und Getränke, Preise mit Monat und Jahr, Kursmodule, Einstellungsseiten mit senkrechten Reitern.

Wann nicht

Nicht für Navigation zwischen Seiten (dann Links in einem nav, keine Tabs). Nicht, wenn Nutzer Inhalte vergleichen müssen (dann untereinander zeigen). Automatische Aktivierung nicht, wenn das Panel erst lädt oder schwer ist; dann manuell. focusgroup nie ohne Fallback, solange nur Chromium es kann.

Varianten

  • Waagerechte Tabs, Auswahl folgt dem Fokus (automatisch)
  • Waagerechte Tabs mit manueller Aktivierung über Enter oder Leertaste
  • Senkrechte Tabs mit aria-orientation=vertical für Einstellungen und Funktions-Kapitel
  • Segment-Schalter aus nativen Radios mit gleitender Pille
  • Werkzeugleiste mit focusgroup="toolbar"

Umsetzung

<div role="tablist" aria-label="Funktionen" focusgroup="tablist nomemory"> mit <button role="tab" id aria-selected aria-controls>, focusgroupstart am gewählten Tab; Panels <section role="tabpanel" aria-labelledby tabindex="0" hidden> (CSS: [role=tabpanel][hidden]{display:none}, sonst überschreibt display:grid das hidden). Erkennung: 'focusGroup' in HTMLElement.prototype. Nativ: keine tabindex-Werte an den Tabs (tabindex=-1 nimmt einen Tab aus der Gruppe). Fallback: gewählter Tab tabindex=0, andere -1; keydown auf der tablist: Pfeil vor/zurück mit Umlauf, Home, End, jeweils preventDefault und focus(). Automatisch: focusin auf einem Tab wählt ihn. Manuell: nur click wählt (Enter und Leertaste lösen bei button click aus). Wählen: aria-selected umschalten, Panels hidden umschalten, focusgroupstart bzw. tabindex=0 umziehen, Pille per translate und width (senkrecht: height) verschieben. Senkrecht: aria-orientation="vertical" und focusgroup="tablist block wrap nomemory". Segment: fieldset mit legend und nativen Radios (opacity 0 über dem Label), Fokusring am Label über :has(input:focus-visible), Pos1/Ende per keydown. Die Produktansicht ist eine figure mit figcaption, damit der Zeitraum-Schalter auch für Screenreader etwas Sichtbares ändert.

Robust umsetzen

Telefon. Waagerechte Tabs dürfen scrollen (overflow-x mit scroll-snap), der gewählte Tab wird ins Bild gerollt. Ziele mindestens 44 px hoch. Senkrechte Tabs werden auf dem Telefon zu waagerechten.

Ohne Bewegung. Indikator springt ohne Gleiten, Panel erscheint ohne Weg.

Barrierefrei. WCAG 2.1.1 Tastatur: alles per Tastatur bedienbar, Tab-Station je Gruppe. 2.4.3 Fokusreihenfolge: Tab landet auf dem gewählten Tab (focusgroupstart bzw. tabindex=0), dann im Panel. 2.4.7 Fokus sichtbar und getrennt von der Auswahl (Fokusring plus Pille). 2.4.11 Fokus nicht verdeckt: gewählten Tab in waagerechten Scroll-Leisten ins Bild rollen. 4.1.2 Name, Rolle, Wert: tablist mit Namen, tab mit aria-selected und aria-controls, tabpanel mit aria-labelledby; die Rollen gibt focusgroup nicht zuverlässig mit. 3.2.1 Bei Fokus: automatische Aktivierung zeigt nur Inhalt, sie wechselt keine Seite. 1.4.11 Kontraste: Pille und Fokusring mindestens 3 : 1. 2.5.8 Zielgröße mindestens 24 × 24 px, hier 44 px.

Code

<!DOCTYPE html>
<!--
  Tabs und Segmente mit Pfeiltasten
  Fundus · Zugang & Robustheit · https://cecodesign.de/lab/fundus/zugang/tabs-pfeiltasten-focusgroup/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Funktionen · Siel</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Geist'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

:root {
  /* Rezept saas-produkt-praezise: Graustufen tragen alles, ein Markenton für Aktion, Fokus, aktiv */
  --papier: #FAFAFA; --karte: #FFFFFF; --tinte: #161616; --leise: #62666D; --linie: #E6E6E8; --spur: #EFEFF1;
  --marke: #5E6AD2;            /* 4,7 : 1 auf Weiß, 4,5 : 1 auf #FAFAFA */
  --app: #08090A; --panel: #161718; --app-linie: #242728; --app-text: #F7F8F8; --app-leise: #8A8F98; --app-marke: #7170FF;
  --gruen: #59D499; --gelb: #FFC533; --rot: #FF6161;
  --kurve: cubic-bezier(0.25, 0.46, 0.45, 0.94);   /* easeOutQuad wie Linear */
  --indikator: 180ms; --panel-dauer: 200ms; --farbe: 120ms;
  --ring: 2px solid var(--marke);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--papier); color: var(--tinte); font: 400 16px/1.6 'Geist', system-ui, sans-serif; letter-spacing: -.006em; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
button, input { font: inherit; color: inherit; }
a { color: inherit; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.ziffern { font-variant-numeric: tabular-nums; }

/* Kopf der Produktseite */
.kopf { max-width: 1180px; margin: 0 auto; padding: 18px clamp(16px, 3vw, 32px); display: flex; align-items: center; gap: 28px; border-bottom: 1px solid var(--linie); }
.marke { display: inline-flex; align-items: center; gap: 9px; font-weight: 600; font-size: 18px; letter-spacing: -.03em; text-decoration: none; }
.marke svg { width: 22px; height: 22px; }
.kopf nav ul { list-style: none; display: flex; gap: 22px; font-size: 14px; color: var(--leise); }
.kopf nav a { text-decoration: none; }
.kopf .knopf { margin-left: auto; }
.knopf { display: inline-flex; align-items: center; min-height: 36px; padding: 0 14px; border-radius: 8px; background: var(--tinte); color: #fff; font-size: 14px; font-weight: 500; text-decoration: none; }
.knopf:focus-visible, .kopf nav a:focus-visible, .marke:focus-visible { outline: var(--ring); outline-offset: 2px; border-radius: 6px; }

.seite { max-width: 1180px; margin: 0 auto; padding: clamp(36px, 6vh, 72px) clamp(16px, 3vw, 32px) 40px; }
.intro { display: grid; grid-template-columns: 1fr 1fr; gap: 24px 48px; align-items: end; margin-bottom: 36px; }
.intro h1 { font-size: clamp(2.25rem, 1.5rem + 2.6vw, 3.5rem); line-height: 1; font-weight: 600; letter-spacing: -.022em; text-wrap: balance; }
.intro p:not(.mono) { color: var(--leise); max-width: 52ch; font-size: 17px; }
.intro .mono { grid-column: 1 / -1; margin-bottom: -8px; }

/* ---------- Werkzeugleiste: Tabs + Segment ---------- */
.leiste { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 18px; }

/* Kern: tablist mit gleitender Pille */
[role="tablist"] { position: relative; display: flex; gap: 2px; padding: 4px; background: var(--spur); border-radius: 12px; isolation: isolate; }
[role="tab"] { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 9px; min-height: 40px; padding: 0 16px; border: 0; border-radius: 9px; background: none; cursor: pointer; font-size: 15px; font-weight: 500; color: var(--leise); white-space: nowrap; transition: color var(--farbe) var(--kurve); }
[role="tab"] .nr { font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .08em; color: var(--leise); }
[role="tab"]:hover { color: var(--tinte); }
[role="tab"][aria-selected="true"] { color: var(--tinte); }
[role="tab"][aria-selected="true"] .nr { color: var(--marke); }
[role="tab"]:focus-visible, [role="tab"].fokus-sim { outline: var(--ring); outline-offset: 2px; }
.pille { position: absolute; z-index: 0; left: 0; top: 4px; height: calc(100% - 8px); width: 0; border-radius: 9px; background: var(--karte); box-shadow: 0 1px 2px rgba(22, 22, 22, .08), 0 0 0 1px rgba(22, 22, 22, .06);
  transition: translate var(--indikator) var(--kurve), width var(--indikator) var(--kurve), height var(--indikator) var(--kurve); }

/* Segment aus nativen Radios */
.segment { border: 0; min-width: 0; display: flex; align-items: center; gap: 12px; }
.segment legend { float: left; }
.segment .spur { position: relative; display: flex; padding: 4px; background: var(--spur); border-radius: 10px; isolation: isolate; }
.segment label { position: relative; z-index: 1; min-height: 36px; min-width: 64px; padding: 0 12px; display: grid; place-items: center; border-radius: 7px; font-size: 14px; font-weight: 500; color: var(--leise); cursor: pointer; transition: color var(--farbe) var(--kurve); }
.segment input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.segment label:has(input:checked) { color: var(--tinte); }
.segment label:has(input:focus-visible), .segment label.fokus-sim { outline: var(--ring); outline-offset: 2px; }
.segment .pille { top: 4px; height: calc(100% - 8px); border-radius: 7px; }

/* ---------- Panels ---------- */
[role="tabpanel"] { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: clamp(20px, 3vw, 44px); align-items: center; padding: clamp(20px, 2.6vw, 32px); background: var(--karte); border: 1px solid var(--linie); border-radius: 18px; outline: none; }
[role="tabpanel"][hidden] { display: none; }
[role="tabpanel"]:focus-visible { outline: var(--ring); outline-offset: 3px; }
[role="tabpanel"].rein { animation: panel-rein var(--panel-dauer) var(--kurve) both; }
@keyframes panel-rein { from { opacity: 0; translate: 0 6px; } }
.text h2 { font-size: clamp(1.5rem, 1.1rem + 1.2vw, 2.1rem); line-height: 1.1; font-weight: 600; letter-spacing: -.018em; margin: 8px 0 10px; text-wrap: balance; }
.text p { color: var(--leise); max-width: 46ch; }
.text ul { list-style: none; margin: 18px 0 20px; display: grid; gap: 8px; font-size: 15px; }
.text li { display: grid; grid-template-columns: 18px 1fr; gap: 10px; align-items: baseline; }
.text li::before { content: ''; width: 10px; height: 10px; border-radius: 3px; background: var(--marke); translate: 0 1px; }
.text a { font-size: 15px; font-weight: 500; color: var(--marke); text-underline-offset: .2em; }
.text a:focus-visible { outline: var(--ring); outline-offset: 3px; border-radius: 3px; }

/* Produkt-UI als HTML */
.app { background: var(--app); color: var(--app-text); border-radius: 14px; border: 1px solid var(--app-linie); overflow: hidden; font-size: 13.5px; box-shadow: 0 30px 60px -30px rgba(8, 9, 10, .45); }
.app-kopf { display: flex; align-items: center; gap: 8px; padding: 11px 14px; border-bottom: 1px solid var(--app-linie); color: var(--app-leise); }
.app-kopf i { width: 9px; height: 9px; border-radius: 50%; background: #2b2d30; }
.app-kopf span { margin-left: 8px; font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .06em; }
.app-kopf b { margin-left: auto; font: 500 11px/1 'Geist Mono', monospace; color: var(--app-leise); font-weight: 500; }
.app-inhalt { padding: 14px; display: grid; gap: 8px; }
.zeile { display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: center; padding: 11px 12px; background: var(--panel); border: 1px solid var(--app-linie); border-radius: 9px; }
.zeile small { display: block; color: var(--app-leise); font-size: 12px; }
.zeile .zahl { font: 500 15px/1 'Geist', sans-serif; font-variant-numeric: tabular-nums; }
.punkt { width: 8px; height: 8px; border-radius: 50%; background: var(--gruen); box-shadow: 0 0 0 3px rgba(89, 212, 153, .15); }
.punkt.gelb { background: var(--gelb); box-shadow: 0 0 0 3px rgba(255, 197, 51, .15); }
.punkt.rot { background: var(--rot); box-shadow: 0 0 0 3px rgba(255, 97, 97, .15); }
.balken { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: #232528; overflow: hidden; }
.balken i { display: block; height: 100%; background: var(--app-marke); border-radius: 2px; transition: width var(--panel-dauer) var(--kurve); }
.chip { font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .06em; padding: 5px 7px; border-radius: 6px; background: rgba(113, 112, 255, .16); color: #B4B3FF; }
.chip.rot { background: rgba(255, 97, 97, .14); color: #FF9C9C; }
.kacheln { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.kachel { background: var(--panel); border: 1px solid var(--app-linie); border-radius: 9px; padding: 12px; }
.kachel small { color: var(--app-leise); font-size: 11.5px; }
.kachel b { display: block; font-size: 22px; font-weight: 500; letter-spacing: -.02em; font-variant-numeric: tabular-nums; margin-top: 2px; }

/* ---------- Tastatur-Pult ---------- */
.pult { margin-top: 18px; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px 24px; padding: 12px 14px; border: 1px dashed #CFD0D4; border-radius: 14px; }
.tasten { display: flex; gap: 5px; flex-wrap: wrap; }
.taste { min-width: 32px; height: 32px; padding: 0 8px; display: grid; place-items: center; border-radius: 7px; background: var(--karte); border: 1px solid var(--linie); box-shadow: 0 1.5px 0 #D6D7DB; font: 500 13px/1 'Geist Mono', monospace; color: var(--leise); transition: background-color var(--farbe) var(--kurve), color var(--farbe) var(--kurve), translate var(--farbe) var(--kurve), box-shadow var(--farbe) var(--kurve); }
.taste[data-taste="Home"], .taste[data-taste="End"] { font-size: 11px; }
.taste.an { background: var(--marke); color: #fff; border-color: var(--marke); box-shadow: 0 0 0 #0000; translate: 0 1.5px; }
.protokoll { font: 500 12px/1.4 'Geist Mono', monospace; color: var(--tinte); letter-spacing: .02em; min-height: 1.4em; }
.protokoll b { color: var(--marke); font-weight: 500; }
.technik { display: inline-flex; align-items: center; gap: 8px; font: 500 11px/1.3 'Geist Mono', monospace; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.technik i { width: 8px; height: 8px; border-radius: 50%; background: var(--gelb); }
.technik.nativ i { background: #1F9D63; }
.steuer { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 20px; padding-top: 10px; border-top: 1px solid var(--linie); }
.steuer [role="group"] { display: flex; gap: 2px; padding: 3px; background: var(--spur); border-radius: 9px; }
.steuer button { border: 0; background: none; min-height: 30px; padding: 0 10px; border-radius: 7px; cursor: pointer; font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--leise); }
.steuer button[aria-pressed="true"] { background: var(--karte); color: var(--tinte); box-shadow: 0 0 0 1px rgba(22, 22, 22, .08); }
.steuer button:focus-visible, .steuer input:focus-visible { outline: var(--ring); outline-offset: 2px; }
.steuer label { display: inline-flex; align-items: center; gap: 8px; font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--leise); cursor: pointer; }
.steuer input { width: 16px; height: 16px; accent-color: var(--marke); }
.werte { margin-left: auto; display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 10.5px; }
.werte b { color: var(--tinte); font-weight: 500; }

/* ---------- Variante senkrecht ---------- */
.bereich { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
.leiste > *, .panels { min-width: 0; }
[data-variante="vertikal"] .bereich { grid-template-columns: 220px minmax(0, 1fr); gap: 18px; align-items: start; }
[data-variante="vertikal"] .leiste { display: contents; }
[data-variante="vertikal"] .segment { grid-column: 2; grid-row: 1; justify-self: end; }
[data-variante="vertikal"] [role="tablist"] { grid-column: 1; grid-row: 1 / span 2; flex-direction: column; }
[data-variante="vertikal"] [role="tab"] { justify-content: flex-start; min-height: 46px; }
[data-variante="vertikal"] .panels { grid-column: 2; grid-row: 2; }
[data-variante="vertikal"] [role="tabpanel"] { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
[data-variante="vertikal"] [role="tablist"] .pille { left: 4px; top: 0; width: calc(100% - 8px) !important; }

/* Reduzierte Bewegung */
.reduziert .pille, .reduziert .balken i { transition: none; }
.reduziert [role="tabpanel"].rein { animation: none; }
.reduziert .taste { transition: none; }

@media (hover: none) {
  [role="tab"]:hover { color: var(--leise); }
  [role="tab"] { min-height: 44px; }
  .segment label { min-height: 40px; }
}
@media (max-width: 860px) {
  .intro { grid-template-columns: 1fr; }
  .leiste { flex-direction: column; align-items: stretch; }
  [role="tablist"] { overflow-x: auto; scrollbar-width: none; scroll-snap-type: x proximity; }
  [role="tablist"]::-webkit-scrollbar { display: none; }
  [role="tab"] { scroll-snap-align: start; }
  .segment { justify-content: space-between; }
  [role="tabpanel"] { grid-template-columns: 1fr; }
  [data-variante="vertikal"] .bereich { grid-template-columns: minmax(0, 1fr); }
  [data-variante="vertikal"] .leiste { display: flex; }
  [data-variante="vertikal"] [role="tablist"] { flex-direction: row; grid-row: auto; }
  [data-variante="vertikal"] .segment, [data-variante="vertikal"] .panels { grid-column: 1; grid-row: auto; }
  [data-variante="vertikal"] [role="tablist"] .pille { top: 4px; left: 0; width: 0; }
  .kopf { flex-wrap: wrap; row-gap: 2px; }
  .kopf nav { order: 3; flex-basis: 100%; }
  .kopf nav a { display: inline-flex; align-items: center; min-height: 44px; }
  .pult { grid-template-columns: 1fr; }
  .werte { margin-left: 0; }
}
@media (max-width: 480px) {
  .kacheln { grid-template-columns: 1fr 1fr; }
  .kachel:last-child { grid-column: 1 / -1; }
  .segment label { min-width: 0; flex: 1; }
  .segment .spur { flex: 1; }
}

/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .kopf, .vorschau .steuer, .vorschau .intro p:not(.mono) { display: none; }
.vorschau .seite { padding-top: 30px; }
.vorschau .intro { margin-bottom: 22px; }
.vorschau .intro h1 { font-size: 40px; }
.vorschau [role="tabpanel"] { padding: 22px; }
.vorschau .text ul { margin: 12px 0 0; }
.vorschau .text a { display: none; }
.vorschau .pult { margin-top: 14px; }

/* Windows-Kontrastmodus: Pille und Leiste bekommen Systemfarben-Kanten, sonst wäre die Auswahl unsichtbar */
@media (forced-colors: active) {
  [role="tablist"], .segment .spur { border: 1px solid CanvasText; }
  .pille { border: 2px solid Highlight; box-sizing: border-box; }
  [role="tab"][aria-selected="true"], .segment label:has(input:checked) { color: Highlight; }
}
</style>
</head>
<body data-variante="auto">
<header class="kopf">
  <a class="marke" href="#">
    <svg viewBox="0 0 22 22" aria-hidden="true"><rect width="22" height="22" rx="6" fill="#161616"/><path d="M5 8h12M5 11h8M5 14h12" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><circle cx="16.5" cy="11" r="1.6" fill="#7170FF"/></svg>
    Siel
  </a>
  <nav aria-label="Hauptmenü"><ul><li><a href="#">Produkt</a></li><li><a href="#">Preise</a></li><li><a href="#">Doku</a></li><li><a href="#">Sicherheit</a></li></ul></nav>
  <a class="knopf" href="#">14 Tage testen</a>
</header>

<main class="seite">
  <div class="intro">
    <p class="mono">Funktionen · 4 Kapitel</p>
    <h1>Vom Posteingang bis ins Archiv.</h1>
    <p>Siel nimmt E-Rechnungen aus jedem Kanal an, prüft sie Feld für Feld und meldet nur, was wirklich hakt. Gebaut in Emden, gehostet in Frankfurt.</p>
  </div>

  <div class="bereich">
    <div class="leiste">
      <div role="tablist" aria-label="Funktionen von Siel" data-tablist>
        <span class="pille" aria-hidden="true" data-pille></span>
        <button type="button" role="tab" id="tab-erfassen" aria-controls="panel-erfassen" aria-selected="true"><span class="nr" aria-hidden="true">01</span>Erfassen</button>
        <button type="button" role="tab" id="tab-pruefen" aria-controls="panel-pruefen" aria-selected="false"><span class="nr" aria-hidden="true">02</span>Prüfen</button>
        <button type="button" role="tab" id="tab-melden" aria-controls="panel-melden" aria-selected="false"><span class="nr" aria-hidden="true">03</span>Melden</button>
        <button type="button" role="tab" id="tab-archiv" aria-controls="panel-archiv" aria-selected="false"><span class="nr" aria-hidden="true">04</span>Archivieren</button>
      </div>

      <fieldset class="segment" data-segment>
        <legend class="mono">Zeitraum</legend>
        <div class="spur">
          <span class="pille" aria-hidden="true" data-pille></span>
          <label><input type="radio" name="zeitraum" value="tag" checked>24 h</label>
          <label><input type="radio" name="zeitraum" value="woche">7 Tage</label>
          <label><input type="radio" name="zeitraum" value="monat">30 Tage</label>
        </div>
      </fieldset>
    </div>

    <div class="panels">
      <section role="tabpanel" id="panel-erfassen" aria-labelledby="tab-erfassen" tabindex="0">
        <div class="text">
          <p class="mono">01 · Erfassen</p>
          <h2>Rechnungen kommen an, egal auf welchem Weg.</h2>
          <p>Ein Postfach, ein SFTP-Ordner, eine Schnittstelle: Siel sammelt alles an einer Stelle und erkennt XRechnung, ZUGFeRD und PDF.</p>
          <ul><li>Eigene Eingangsadresse je Mandant</li><li>Doppelte Dateien werden erkannt</li><li>Kein Abruf aus fremden Clouds nötig</li></ul>
          <a href="#">Mehr zu Eingangskanälen</a>
        </div>
        <figure class="app">
          <figcaption class="app-kopf"><i aria-hidden="true"></i><i aria-hidden="true"></i><i aria-hidden="true"></i><span>siel / quellen</span><b data-zeitraum-label>letzte 24 h</b></figcaption>
          <div class="app-inhalt">
            <div class="zeile"><div>rechnung@nordwerk.de<small>E-Mail · XRechnung, PDF</small></div><span class="punkt" aria-hidden="true"></span><span class="zahl" data-zahl="mail"></span></div>
            <div class="zeile"><div>sftp / grosshandel-ems<small>SFTP · ZUGFeRD 2.3</small></div><span class="punkt" aria-hidden="true"></span><span class="zahl" data-zahl="sftp"></span></div>
            <div class="zeile"><div>api / webshop<small>REST · JSON nach EN 16931</small></div><span class="punkt gelb" aria-hidden="true"></span><span class="zahl" data-zahl="api"></span></div>
          </div>
        </figure>
      </section>

      <section role="tabpanel" id="panel-pruefen" aria-labelledby="tab-pruefen" tabindex="0" hidden>
        <div class="text">
          <p class="mono">02 · Prüfen</p>
          <h2>Jedes Pflichtfeld, bevor es gebucht wird.</h2>
          <p>Siel prüft gegen EN 16931 und Ihre eigenen Regeln. Fehler landen nicht in der Buchhaltung, sondern mit Begründung beim Absender.</p>
          <ul><li>Pflichtfelder und Rechenregeln</li><li>USt-IdNr. und Bankverbindung</li><li>Eigene Regeln ohne Code</li></ul>
          <a href="#">Alle Prüfregeln ansehen</a>
        </div>
        <figure class="app">
          <figcaption class="app-kopf"><i aria-hidden="true"></i><i aria-hidden="true"></i><i aria-hidden="true"></i><span>siel / regeln</span><b data-zeitraum-label>letzte 24 h</b></figcaption>
          <div class="app-inhalt">
            <div class="zeile"><div>Pflichtfelder EN 16931<small>bestanden</small></div><span></span><span class="zahl" data-zahl="pflicht"></span><div class="balken"><i data-balken="pflicht"></i></div></div>
            <div class="zeile"><div>USt-IdNr. gültig<small>bestanden</small></div><span></span><span class="zahl" data-zahl="ust"></span><div class="balken"><i data-balken="ust"></i></div></div>
            <div class="zeile"><div>Rechnungsnummer doppelt<small>angehalten</small></div><span class="chip rot">prüfen</span><span class="zahl" data-zahl="doppelt"></span></div>
          </div>
        </figure>
      </section>

      <section role="tabpanel" id="panel-melden" aria-labelledby="tab-melden" tabindex="0" hidden>
        <div class="text">
          <p class="mono">03 · Melden</p>
          <h2>Nur wer gebraucht wird, wird gestört.</h2>
          <p>Abweichungen gehen an die richtige Person, gebündelt statt einzeln. Der Rest läuft still durch.</p>
          <ul><li>Regeln je Team und Lieferant</li><li>Tägliche Zusammenfassung um 8 Uhr</li><li>Eskalation nach 48 Stunden</li></ul>
          <a href="#">Benachrichtigungen einrichten</a>
        </div>
        <figure class="app">
          <figcaption class="app-kopf"><i aria-hidden="true"></i><i aria-hidden="true"></i><i aria-hidden="true"></i><span>siel / meldungen</span><b data-zeitraum-label>letzte 24 h</b></figcaption>
          <div class="app-inhalt">
            <div class="zeile"><div>Buchhaltung<small>E-Mail · täglich 08:00</small></div><span class="chip">gebündelt</span><span class="zahl" data-zahl="buha"></span></div>
            <div class="zeile"><div>Einkauf<small>Chat · sofort bei Preisabweichung</small></div><span class="chip rot">sofort</span><span class="zahl" data-zahl="einkauf"></span></div>
            <div class="zeile"><div>Geschäftsführung<small>Wochenbericht · freitags</small></div><span class="chip">bericht</span><span class="zahl" data-zahl="gf"></span></div>
          </div>
        </figure>
      </section>

      <section role="tabpanel" id="panel-archiv" aria-labelledby="tab-archiv" tabindex="0" hidden>
        <div class="text">
          <p class="mono">04 · Archivieren</p>
          <h2>Zehn Jahre aufbewahrt, kein Byte verändert.</h2>
          <p>Jede Rechnung liegt mit Prüfprotokoll im Archiv, durchsuchbar und für die Betriebsprüfung exportierbar.</p>
          <ul><li>Unveränderbar gespeichert, mit Prüfsumme</li><li>Volltextsuche über alle Jahre</li><li>Export für die Betriebsprüfung</li></ul>
          <a href="#">So funktioniert das Archiv</a>
        </div>
        <figure class="app">
          <figcaption class="app-kopf"><i aria-hidden="true"></i><i aria-hidden="true"></i><i aria-hidden="true"></i><span>siel / archiv</span><b data-zeitraum-label>letzte 24 h</b></figcaption>
          <div class="app-inhalt">
            <div class="kacheln">
              <div class="kachel"><small>Neu archiviert</small><b data-zahl="neu"></b></div>
              <div class="kachel"><small>Speicher</small><b data-zahl="speicher"></b></div>
              <div class="kachel"><small>Exporte</small><b data-zahl="export"></b></div>
            </div>
            <div class="zeile"><div>RE-2026-11-0412.xml<small>Großhandel Ems · Prüfsumme 9f3c…e21a</small></div><span class="punkt" aria-hidden="true"></span><span class="chip">10 J.</span></div>
          </div>
        </figure>
      </section>
    </div>
  </div>

  <div class="pult">
    <div class="tasten" aria-hidden="true" data-tasten>
      <span class="taste" data-taste="Tab">⇥</span><span class="taste" data-taste="ArrowLeft">←</span><span class="taste" data-taste="ArrowRight">→</span><span class="taste" data-taste="ArrowUp">↑</span><span class="taste" data-taste="ArrowDown">↓</span><span class="taste" data-taste="Home">Pos1</span><span class="taste" data-taste="End">Ende</span><span class="taste" data-taste="Enter">↵</span><span class="taste" data-taste=" ">␣</span>
    </div>
    <p class="protokoll" aria-hidden="true" data-protokoll>Mit Tab in die Reiter, dann Pfeiltasten.</p>
    <p class="technik" data-technik><i></i><span></span></p>
    <div class="steuer">
      <div role="group" aria-label="Variante der Demo">
        <button type="button" data-v="auto" aria-pressed="true">Automatisch</button>
        <button type="button" data-v="manuell" aria-pressed="false">Manuell</button>
        <button type="button" data-v="vertikal" aria-pressed="false">Senkrecht</button>
      </div>
      <label><input type="checkbox" data-fallback> Fallback erzwingen</label>
      <p class="werte mono" data-werte aria-hidden="true"></p>
    </div>
  </div>
</main>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const liste = $('[data-tablist]'), tabs = $$('[role="tab"]', liste), pille = $('[data-pille]', liste);
  const segment = $('[data-segment]'), radios = $$('input', segment), segPille = $('[data-pille]', segment);
  const protokoll = $('[data-protokoll]');

  // Technik: focusgroup nativ (Chrome 150+) oder roving tabindex
  const KANN = 'focusGroup' in HTMLElement.prototype;
  let erzwungen = false, variante = 'auto';
  const nativ = () => KANN && !erzwungen;
  const senkrecht = () => variante === 'vertikal' && !matchMedia('(max-width: 860px)').matches;

  function aufbauen() {
    if (nativ()) liste.setAttribute('focusgroup', senkrecht() ? 'tablist block wrap nomemory' : 'tablist nomemory');
    else liste.removeAttribute('focusgroup');
    if (senkrecht()) liste.setAttribute('aria-orientation', 'vertical'); else liste.removeAttribute('aria-orientation');
    const t = gewaehlt();
    tabs.forEach(b => {
      b.toggleAttribute('focusgroupstart', b === t);
      if (nativ()) b.removeAttribute('tabindex');           // tabindex=-1 würde den Tab aus der Gruppe nehmen
      else b.tabIndex = b === t ? 0 : -1;                    // roving tabindex
    });
    const tech = $('[data-technik]');
    tech.classList.toggle('nativ', nativ());
    tech.querySelector('span').textContent = nativ() ? 'focusgroup · nativ' : KANN ? 'roving tabindex · erzwungen' : 'roving tabindex · Fallback';
    pillen();
  }
  const gewaehlt = () => tabs.find(b => b.getAttribute('aria-selected') === 'true');

  function waehle(tab, still) {
    const alt = gewaehlt();
    tabs.forEach(b => {
      const an = b === tab;
      b.setAttribute('aria-selected', String(an));
      b.toggleAttribute('focusgroupstart', an);
      if (!nativ()) b.tabIndex = an ? 0 : -1;
      const p = document.getElementById(b.getAttribute('aria-controls'));
      p.hidden = !an;
      if (an && alt !== tab && !still) { p.classList.remove('rein'); void p.offsetWidth; p.classList.add('rein'); }
    });
    pillen();
    inSicht(tab);
  }
  // Waagerechte Leiste auf dem Telefon: gewählten Tab ins Bild rollen (nur die Leiste, nicht die Seite)
  function inSicht(tab) {
    if (liste.scrollWidth <= liste.clientWidth) return;
    const l = tab.offsetLeft - 16, r = tab.offsetLeft + tab.offsetWidth + 16;
    if (l < liste.scrollLeft) liste.scrollTo({ left: l, behavior: reduziert() ? 'auto' : 'smooth' });
    else if (r > liste.scrollLeft + liste.clientWidth) liste.scrollTo({ left: r - liste.clientWidth, behavior: reduziert() ? 'auto' : 'smooth' });
  }
  function pillen() {
    const t = gewaehlt();
    if (senkrecht()) { pille.style.translate = `0 ${t.offsetTop}px`; pille.style.height = t.offsetHeight + 'px'; pille.style.width = ''; }
    else { pille.style.translate = `${t.offsetLeft}px 0`; pille.style.width = t.offsetWidth + 'px'; pille.style.height = ''; }
    const r = radios.find(i => i.checked).parentElement;
    segPille.style.translate = `${r.offsetLeft}px 0`; segPille.style.width = r.offsetWidth + 'px';
  }

  // Fallback-Tastatur (nur ohne focusgroup): Pfeile mit Umlauf, Pos1, Ende
  liste.addEventListener('keydown', e => {
    if (nativ()) return;
    const i = tabs.indexOf(document.activeElement);
    if (i < 0) return;
    const vor = senkrecht() ? 'ArrowDown' : 'ArrowRight', zurueck = senkrecht() ? 'ArrowUp' : 'ArrowLeft';
    let z = null;
    if (e.key === vor) z = (i + 1) % tabs.length;
    else if (e.key === zurueck) z = (i - 1 + tabs.length) % tabs.length;
    else if (e.key === 'Home') z = 0;
    else if (e.key === 'End') z = tabs.length - 1;
    if (z === null) return;
    e.preventDefault();
    tabs[z].focus();
  });
  // Automatisch: Auswahl folgt dem Fokus. Manuell: nur Klick, Enter, Leertaste.
  liste.addEventListener('focusin', e => {
    const t = e.target.closest('[role="tab"]');
    if (t && variante !== 'manuell') waehle(t);
  });
  tabs.forEach(t => t.addEventListener('click', () => waehle(t)));

  // Segment: native Radios wählen mit Pfeilen von selbst; Pos1 und Ende ergänzen
  segment.addEventListener('keydown', e => {
    if (e.key !== 'Home' && e.key !== 'End') return;
    e.preventDefault();
    const r = e.key === 'Home' ? radios[0] : radios[radios.length - 1];
    r.checked = true; r.focus(); r.dispatchEvent(new Event('change', { bubbles: true }));
  });
  const DATEN = {
    tag:   { label: 'letzte 24 h', mail: '214', sftp: '96', api: '1.380', pflicht: '99,2 %', ust: '97,8 %', doppelt: '3', buha: '1', einkauf: '2', gf: '0', neu: '1.687', speicher: '2,1 GB', export: '0', b: [99.2, 97.8] },
    woche: { label: 'letzte 7 Tage', mail: '1.402', sftp: '611', api: '9.954', pflicht: '98,7 %', ust: '96,9 %', doppelt: '17', buha: '5', einkauf: '11', gf: '1', neu: '11.950', speicher: '14,8 GB', export: '2', b: [98.7, 96.9] },
    monat: { label: 'letzte 30 Tage', mail: '6.118', sftp: '2.540', api: '41.207', pflicht: '98,9 %', ust: '97,1 %', doppelt: '64', buha: '22', einkauf: '39', gf: '4', neu: '49.854', speicher: '61,3 GB', export: '6', b: [98.9, 97.1] }
  };
  function zeitraum() {
    const d = DATEN[radios.find(i => i.checked).value];
    $$('[data-zahl]').forEach(el => el.textContent = d[el.dataset.zahl]);
    $$('[data-zeitraum-label]').forEach(el => el.textContent = d.label);
    $('[data-balken="pflicht"]').style.width = d.b[0] + '%';
    $('[data-balken="ust"]').style.width = d.b[1] + '%';
    pillen();
  }
  segment.addEventListener('change', zeitraum);

  // Tastatur-Anzeige (auch bei echter Bedienung)
  function taste(k, text) {
    const el = $(`[data-taste="${k}"]`);
    if (el) { el.classList.add('an'); setTimeout(() => el.classList.remove('an'), 260); }
    if (text) protokoll.innerHTML = text;
  }
  const NAME = { Tab: '⇥', ArrowLeft: '←', ArrowRight: '→', ArrowUp: '↑', ArrowDown: '↓', Home: 'Pos1', End: 'Ende', Enter: '↵', ' ': '␣' };
  document.addEventListener('keydown', e => {
    if (!NAME[e.key] || html.classList.contains('vorfuehrung')) return;
    taste(e.key);
    setTimeout(() => {
      const a = document.activeElement;
      if (a.getAttribute('role') === 'tab') protokoll.innerHTML = `<b>${NAME[e.key]}</b>  Fokus: ${a.textContent.replace(/^\d+/, '')}${a.getAttribute('aria-selected') === 'true' ? ' · gewählt' : ' · nicht gewählt'}`;
      else if (a.name === 'zeitraum') protokoll.innerHTML = `<b>${NAME[e.key]}</b>  Zeitraum: ${a.parentElement.textContent.trim()}`;
      else if (a.getAttribute('role') === 'tabpanel') protokoll.innerHTML = `<b>${NAME[e.key]}</b>  im Panel ${a.querySelector('.mono').textContent}`;
    });
  }, true);

  // Varianten (?v=auto | manuell | vertikal)
  const WERTE = {
    gemeinsam: '<span><b>easeOutQuad</b> cubic-bezier(0.25, 0.46, 0.45, 0.94)</span><span>Pille <b>180 ms</b></span><span>Panel <b>200 ms</b> · 6 px</span>',
    auto: '<span>Auswahl folgt Fokus</span>', manuell: '<span>Enter/Leertaste wählt</span>', vertikal: '<span>focusgroup="tablist block"</span>'
  };
  const schalter = $$('[data-v]');
  function setzeVariante(v, url) {
    variante = ['auto', 'manuell', 'vertikal'].includes(v) ? v : 'auto';
    document.body.dataset.variante = variante;
    schalter.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === variante)));
    $('[data-werte]').innerHTML = WERTE.gemeinsam + WERTE[variante];
    if (url) { const q = new URLSearchParams(location.search); q.set('v', variante); history.replaceState(null, '', '?' + q.toString()); }
    aufbauen();
  }
  schalter.forEach(b => b.addEventListener('click', () => { const lief = html.classList.contains('vorfuehrung'); neustart(); setzeVariante(b.dataset.v, true); if (lief) vorfuehren(); }));
  $('[data-fallback]').addEventListener('change', e => { erzwungen = e.target.checked; aufbauen(); });
  addEventListener('resize', () => { aufbauen(); });
  document.fonts.ready.then(pillen);

  // ---------- Vorführung: gedachte Tastatur ----------
  let zeit = [], laeuft = false;
  const nach = (ms, f) => zeit.push(setTimeout(f, ms));
  function zeige(el) { $$('.fokus-sim').forEach(x => x.classList.remove('fokus-sim')); if (el) el.classList.add('fokus-sim'); }
  const tabName = t => t.textContent.replace(/^\d+/, '');
  function schritt(t, k, ziel, waehlen, text) {
    nach(t, () => {
      zeige(ziel);
      if (ziel.getAttribute('role') === 'tab' && waehlen) waehle(ziel);
      if (ziel.tagName === 'LABEL') { const r = ziel.querySelector('input'); r.checked = true; zeitraum(); }
      taste(k, `<b>${NAME[k]}</b>  ${text}`);
    });
  }
  function runde() {
    waehle(tabs[0], true); radios[0].checked = true; zeitraum(); zeige(null);
    const auto = variante !== 'manuell', vor = variante === 'vertikal' && senkrecht() ? 'ArrowDown' : 'ArrowRight';
    const lab = radios.map(r => r.parentElement);
    let t = 500;
    const f = (i, k, w) => schritt(t += 1150, k, tabs[i], w, `Fokus: ${tabName(tabs[i])}${w ? ' · gewählt' : ' · Auswahl bleibt'}`);
    schritt(t, 'Tab', tabs[0], false, 'Tab landet auf dem gewählten Reiter');
    f(1, vor, auto);
    f(2, vor, auto);
    if (!auto) schritt(t += 1150, 'Enter', tabs[2], true, `Enter wählt: ${tabName(tabs[2])}`);
    f(3, 'End', auto);
    f(0, 'Home', auto);
    if (!auto) schritt(t += 1150, ' ', tabs[0], true, `Leertaste wählt: ${tabName(tabs[0])}`);
    schritt(t += 1300, 'Tab', lab[0], false, 'Tab springt zur nächsten Gruppe: Zeitraum');
    schritt(t += 1150, 'ArrowRight', lab[1], true, 'Zeitraum: 7 Tage (natives Radio)');
    schritt(t += 1150, 'ArrowRight', lab[2], true, 'Zeitraum: 30 Tage');
    schritt(t += 1150, 'Home', lab[0], true, 'Pos1: 24 h');
    nach(t + 1500, runde);
  }
  function vorfuehren() { neustart(); html.classList.add('vorfuehrung'); laeuft = true; runde(); }
  function pause() { zeit.forEach(clearTimeout); zeit = []; laeuft = false; }
  function neustart() { pause(); html.classList.remove('vorfuehrung'); zeige(null); waehle(tabs[0], true); radios[0].checked = true; zeitraum(); protokoll.textContent = 'Mit Tab in die Reiter, dann Pfeiltasten.'; }
  function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) { laeuft = true; runde(); } }

  setzeVariante(new URLSearchParams(location.search).get('v'), false);
  zeitraum();

})();
</script>
</body>
</html>