DEIN TÄGLICHES UI/UX-COACHING / LEKTION 15

Dark Mode: Oberflächenhierarchie, semantische Farben und Accessibility

Ein stimmiger Dark Mode entsteht nicht durch das Umkehren von Farben. Du stimmst Oberflächen, Text, Aktionen und Status neu aufeinander ab – mit derselben Aufgabe und denselben funktionalen Anforderungen.

ca. 15–17 Minuten inklusive Übung

Dein Lernziel

Du entwickelst eine nachvollziehbare Oberflächenhierarchie, arbeitest mit semantischen Farb-Tokens und prüfst Kontraste, Diagramme und Interaktionszustände in Light und Dark.

01 / MINI-LEKTION · 3 MINUTEN

Dunkel ist noch keine Hierarchie.

Stell dir ein Analytics-Dashboard mit Seitenhintergrund, KPI-Karten und einem hervorgehobenen Analysepanel vor. Wenn alle Flächen fast gleich aussehen, bleibt ihre Beziehung unklar. Ein starker Schatten allein hilft auf einer dunklen Umgebung wenig. In dieser Übung erzeugst du Ordnung durch abgestufte Flächen, gezielte Konturen und Typografie.

Text braucht Gewichtung, nicht bloß Weiß.

Reines Weiß auf Schwarz hat einen hohen rechnerischen Kontrast. Daraus folgt weder, dass es grundsätzlich falsch ist, noch dass es sich für jede Situation am besten eignet. Mit einem hellen Primärtext und einem etwas zurückgenommenen Sekundärtext kannst du Prioritäten ausdrücken. Ob das ruhiger wirkt, ist eine Gestaltungsannahme, die du im Nutzungskontext prüfen solltest.

„Sekundär“ bedeutet nicht „schwer lesbar“. Eine kleine Quellenangabe braucht weiterhin ausreichenden Kontrast. Umgekehrt stellt ein bestandener Mindestwert noch nicht sicher, dass die wichtige Kennzahl zuerst auffällt.

Ein Akzent erfüllt eine Aufgabe.

Ein Blau, das auf Weiß gut funktioniert, kann auf dunklem Grund eine andere Wirkung haben. Statt einen Farbwert überall zu übernehmen, benennst du seine Rolle: Aktion, Beschriftung auf einer Aktion, Fokus oder Warnung. Die Komponenten verwenden dieselben Rollen; pro Theme erhalten sie passende Werte.

02 / DEINE CHALLENGE · 8 MINUTEN

Gestalte ein Dashboard als zusammenhängendes System.

Dein fiktives SaaS-Dashboard enthält vier KPI-Karten, eine Navigation, ein Liniendiagramm und einen kritischen Budgethinweis. Arbeite in Figma oder CSS. Es geht um eine begründete erste Variante, nicht um ein vollständig validiertes Designsystem.

  1. Farbarchitektur · 2 Minuten: Definiere background, surface, text-primary, text-secondary und border. Ergänze Aktion, Aktionsbeschriftung, Fokus und Warnzustand. Brauchst du für das Analysepanel eine weitere Fläche?
  2. Hierarchie · 3 Minuten: Gestalte eine KPI-Karte und den Warnhinweis. Begründe, wo Hintergrund, Kontur, Schrift und Akzent die Orientierung übernehmen.
  3. Accessibility · 2 Minuten: Prüfe konkrete Vordergrund-Hintergrund-Paare, auch für kleine Texte und Buttons. Formuliere die Warnung so, dass ihre Bedeutung ohne Rot erhalten bleibt.
  4. Belastungstest · 1 Minute: Übertrage die Tokens auf sechs Diagrammreihen. Reicht die Farbauswahl? Ergänze bei Bedarf Linienmuster, Symbole und eine Datentabelle.

Zusatzchallenge: Gestalte den ausgewählten Navigationseintrag, Hover und Tastaturfokus. Eine Auswahl bleibt bestehen; Hover beschreibt den Zeigerkontakt; Fokus zeigt das aktuelle Tastaturziel. Diese Zustände können gleichzeitig auftreten und müssen zusammen funktionieren.

03 / BEDIENBARE MUSTERLÖSUNG

Gleiche Inhalte, bewusst andere Farben.

Vergleiche Light und Dark. Wähle anschließend „System“ und simuliere eine andere Systempräferenz. Stelle dann ausdrücklich Light ein: Diese manuelle Entscheidung soll auch bei simuliertem Dark bestehen bleiben. Das Labor speichert keine Auswahl und ändert nur sein eigenes Dashboard.

Aktiv: Dark · manuelle Auswahl hat Vorrang.

METRICS / FIKTIVES DASHBOARD

Überblick behalten.

Vier Beispielwochen · alle Werte sind erfunden.

Übersicht: Kennzahlen, Budgethinweis und Kanalvergleich.

Besuche

750.000in vier Beispielwochen

Registrierungen

8.540in vier Beispielwochen

Abschlussrate

1,14 %Registrierungen / Besuche

Kosten

12.400 €Budget: 10.000 €

Sechs Kanäle, ein Vergleich.

Besuche in Tsd. · KW 1–4 · synthetische Werte

Besuche nach Kanal in vier WochenSechs Reihen mit unterschiedlichen Linienmustern und Symbolen. Die exakten Werte stehen in der Datentabelle direkt darunter. Suche steigt von 42 auf 49 Tsd., Direkt von 25 auf 40 Tsd.02550KW 1KW 2KW 3KW 4●●●●■■■■▲▲▲▲◆◆◆◆✚✚✚✚××××
  • Direkt
  • Suche
  • Empfehlung
  • Newsletter
  • Social
  • Partner
Alle Diagrammwerte als Tabelle
Besuche in Tausend, fiktive Beispieldaten
KanalKW 1KW 2KW 3KW 4
● Direkt25312940
■ Suche42374649
▲ Empfehlung18232030
◆ Newsletter31283633
✚ Social11162421
× Partner35423845
Aktuelle Tokens und berechnete Kontraste

Deckende sRGB-Farben der aktiven Variante. Die Bewertung nutzt den ungerundeten Wert; die Anzeige hat zwei Nachkommastellen. Ein bestandenes Paar ist kein vollständiger Accessibility-Nachweis.

background
#0b1020
surface
#151c2d
raised
#202b42
primary
#f1f5f9
secondary
#b6c2d2
border
#334155
control
#94a3b8
accent
#a5b4fc
onAccent
#111827
danger
#fda4af
dangerSurface
#291b29
focus
#a5b4fc
  • Primärtext / background17,28:1 · Mindestwert 4,5:1 · erfüllt
  • Sekundärtext / background10,49:1 · Mindestwert 4,5:1 · erfüllt
  • Fokus / background9,50:1 · Mindestwert 3:1 · erfüllt
  • Primärtext / surface15,51:1 · Mindestwert 4,5:1 · erfüllt
  • Sekundärtext / surface9,42:1 · Mindestwert 4,5:1 · erfüllt
  • Fokus / surface8,52:1 · Mindestwert 3:1 · erfüllt
  • Primärtext / raised12,90:1 · Mindestwert 4,5:1 · erfüllt
  • Sekundärtext / raised7,83:1 · Mindestwert 4,5:1 · erfüllt
  • Fokus / raised7,09:1 · Mindestwert 3:1 · erfüllt
  • Aktionsbeschriftung / Akzent8,90:1 · Mindestwert 4,5:1 · erfüllt
  • Warntext / Warnfläche8,66:1 · Mindestwert 4,5:1 · erfüllt
  • Control-Kontur / Kartenfläche6,63:1 · Mindestwert 3:1 · erfüllt

Die zarte border-Kontur ordnet dekorative Karten. Identifizierbare Controls erhalten die stärkere control-Kontur. Die sechs Datenreihen werden zusätzlich durch Muster, Symbole und Tabellenwerte unterschieden.

Die Kennzahlen und Diagrammwerte sind synthetisch. Die sechs Kanäle ergeben zusammen 750.000 Besuche in vier Beispielwochen. Daraus und aus 8.540 Registrierungen folgt die gerundete Abschlussrate von 1,14 %. Die Diagrammreihen bleiben über Linienmuster, Symbole, Legende und exakte Tabellenwerte zuordenbar. Das ist eine eigene Musterlösung, kein dokumentierter Produkttest.

04 / TECHNISCHER IMPULS · 3 MINUTEN

Komponenten kennen Bedeutungen statt Themes.

Designentscheidungen und vollständigen Code öffnen

1. Drei Ebenen, zurückhaltende Konturen.

Der Seitenhintergrund ist in Dark am dunkelsten. KPI-Karten liegen auf surface, das Analysepanel auf der helleren Fläche raised. Abstände und Überschriften ergänzen diese Ordnung. Die feinen border-Linien sind dekorativ; Bedienelemente erhalten mit control eine stärkere Kontur.

Mehr Ebenen wären nicht automatisch besser. Wenn jede Karte eine eigene Leuchtfarbe erhält, verliert das hervorgehobene Panel seinen Vorrang. Prüfe die Hierarchie auch in Graustufen und mit deinen echten Inhalten.

2. Aktion und Beschriftung gehören zusammen.

Light nutzt einen dunkleren Akzent mit weißer Beschriftung. Dark nutzt einen helleren Akzent mit dunkler Beschriftung. Nur den Buttonhintergrund aufzuhellen und weiße Schrift beizubehalten, könnte seinen Textkontrast verschlechtern. Deshalb gehört onAccent als eigene Rolle zum System.

Der Budgethinweis nennt Problem, Betrag und nächsten Schritt ausdrücklich. Rot unterstützt die bereits lesbare Aussage. „Prüfschritte ansehen“ öffnet tatsächlich die beispielhaften Schritte; es wird keine echte Budgetänderung ausgelöst.

3. Auswahl, Hover und Fokus bleiben unterscheidbar.

Der aktive Navigationseintrag hat eine gefüllte Akzentfläche, ein Häkchen und aria-pressed. Hover ergänzt eine Unterstreichung. Tastaturfokus erhält eine abgesetzte Kontur. Die Fokusmarkierung bleibt auch beim ausgewählten Eintrag sichtbar. Die Buttons wechseln nur den erklärenden Dashboard-Ausschnitt; sie geben sich nicht als echte App-Routen aus.

4. Die manuelle Entscheidung hat Vorrang.

Die Auswahl lautet Light, Dark oder System. Nur bei System folgt der wirksame Modus der Systempräferenz. prefers-color-scheme ermöglicht das Auslesen dieser Präferenz. Das Labor beobachtet Änderungen mit matchMedia und entfernt den Listener beim Verlassen. MDN: prefers-color-scheme.

color-scheme teilt dem Browser den verwendbaren Modus mit, etwa für native Controls und Scrollbars. Es färbt ein eigenes Design nicht automatisch vollständig um; dafür stehen die semantischen Tokens. MDN: color-scheme.

Die Systemsimulation ist ausdrücklich eine Testhilfe. Die Betriebssystemeinstellung wird dabei nicht verändert. In einer echten Anwendung könntest du eine manuelle Wahl speichern und einen klaren Weg zurück zu System anbieten. Für diese Lektion bleibt sie nur im Komponentenzustand. Ein Produkt sollte außerdem den initialen Modus vor der ersten Darstellung klären, damit kein unerwünschter Theme-Wechsel sichtbar aufblitzt.

5. Die Kontrastprüfung nutzt tatsächliche Paare.

Das Modell berechnet relative sRGB-Luminanz und daraus das Kontrastverhältnis. Primär- und Sekundärtext werden auf allen drei Flächen geprüft, Aktionsbeschriftung auf dem Akzent und Warntext auf der Warnfläche. Die Anzeige rundet, die Entscheidung dagegen nicht. Transparenz, Fotos und Verläufe sind nicht Teil dieses einfachen Rechners.

coaching-dark-mode.ts

export type LessonTheme = "light" | "dark";
export type ThemeChoice = LessonTheme | "system";
export function resolveLessonTheme(choice: ThemeChoice, system: LessonTheme): LessonTheme {
  return choice === "system" ? system : choice;
}
export const lessonThemeTokens = {
  light: { background: "#f8fafc", surface: "#ffffff", raised: "#eef2ff", primary: "#0f172a", secondary: "#475569", border: "#cbd5e1", control: "#64748b", accent: "#4338ca", onAccent: "#ffffff", danger: "#b42318", dangerSurface: "#fff1f2", focus: "#4338ca" },
  dark: { background: "#0b1020", surface: "#151c2d", raised: "#202b42", primary: "#f1f5f9", secondary: "#b6c2d2", border: "#334155", control: "#94a3b8", accent: "#a5b4fc", onAccent: "#111827", danger: "#fda4af", dangerSurface: "#291b29", focus: "#a5b4fc" },
} as const;
export function relativeLuminance(hex: string): number {
  if (!/^#[0-9a-f]{6}$/i.test(hex)) throw new Error("Expected opaque six-digit hex color");
  const channels = [1, 3, 5].map(index => {
    const value = parseInt(hex.slice(index, index + 2), 16) / 255;
    return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
  });
  return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
}
export function themeContrast(foreground: string, background: string): number {
  const a = relativeLuminance(foreground), b = relativeLuminance(background);
  return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
}
export function lessonContrastChecks(theme: LessonTheme) {
  const t = lessonThemeTokens[theme];
  return [
    ...(["background", "surface", "raised"] as const).flatMap(surface => [
      { label: `Primärtext / ${surface}`, foreground: t.primary, background: t[surface], minimum: 4.5 },
      { label: `Sekundärtext / ${surface}`, foreground: t.secondary, background: t[surface], minimum: 4.5 },
      { label: `Fokus / ${surface}`, foreground: t.focus, background: t[surface], minimum: 3 },
    ]),
    { label: "Aktionsbeschriftung / Akzent", foreground: t.onAccent, background: t.accent, minimum: 4.5 },
    { label: "Warntext / Warnfläche", foreground: t.danger, background: t.dangerSurface, minimum: 4.5 },
    { label: "Control-Kontur / Kartenfläche", foreground: t.control, background: t.surface, minimum: 3 },
  ].map(pair => ({ ...pair, ratio: themeContrast(pair.foreground, pair.background) }));
}
export const lessonChartSeries = [
  { name: "Direkt", symbol: "●", dash: "", values: [25, 31, 29, 40], light: "#4338ca", dark: "#a5b4fc" },
  { name: "Suche", symbol: "■", dash: "10 5", values: [42, 37, 46, 49], light: "#0f766e", dark: "#5eead4" },
  { name: "Empfehlung", symbol: "▲", dash: "3 5", values: [18, 23, 20, 30], light: "#a14407", dark: "#fcd34d" },
  { name: "Newsletter", symbol: "◆", dash: "14 4 3 4", values: [31, 28, 36, 33], light: "#be185d", dark: "#f9a8d4" },
  { name: "Social", symbol: "✚", dash: "1 5", values: [11, 16, 24, 21], light: "#1d4ed8", dark: "#7dd3fc" },
  { name: "Partner", symbol: "×", dash: "8 4 2 4 2 4", values: [35, 42, 38, 45], light: "#6d28d9", dark: "#c4b5fd" },
] as const;

coaching-dark-mode.tsx

"use client";

import { useEffect, useId, useState, type CSSProperties } from "react";
import { lessonChartSeries, lessonContrastChecks, lessonThemeTokens, resolveLessonTheme, type LessonTheme, type ThemeChoice } from "@/lib/coaching-dark-mode";
import "./coaching-dark-mode.css";

export function CoachingDarkMode() {
  const id = useId();
  const [choice, setChoice] = useState<ThemeChoice>("dark");
  const [system, setSystem] = useState<LessonTheme>("light");
  const [simulation, setSimulation] = useState<"real" | LessonTheme>("real");
  const [view, setView] = useState("Übersicht");
  const [measures, setMeasures] = useState(false);
  useEffect(() => {
    const query = window.matchMedia("(prefers-color-scheme: dark)");
    function update() { setSystem(query.matches ? "dark" : "light"); }
    update();
    query.addEventListener("change", update);
    return () => query.removeEventListener("change", update);
  }, []);
  const effectiveSystem = simulation === "real" ? system : simulation;
  const theme = resolveLessonTheme(choice, effectiveSystem);
  const tokens = lessonThemeTokens[theme];
  const style = Object.fromEntries(Object.entries(tokens).map(([key, value]) => [`--dm-${key}`, value])) as CSSProperties;
  const checks = lessonContrastChecks(theme);
  return <div className="dark-mode-lab">
    <div className="dark-mode-controls">
      <label htmlFor={`${id}-choice`}>Theme im Lernbeispiel<select id={`${id}-choice`} value={choice} onChange={event => setChoice(event.target.value as ThemeChoice)}><option value="light">Light</option><option value="dark">Dark</option><option value="system">System</option></select></label>
      <label htmlFor={`${id}-system`}>Systempräferenz testen<select id={`${id}-system`} value={simulation} onChange={event => setSimulation(event.target.value as "real" | LessonTheme)}><option value="real">Echte Systemeinstellung</option><option value="light">Light simulieren</option><option value="dark">Dark simulieren</option></select></label>
    </div>
    <p className="dark-mode-resolution" role="status">Aktiv: {theme === "dark" ? "Dark" : "Light"} · {choice === "system" ? "folgt der Systempräferenz" : "manuelle Auswahl hat Vorrang"}{simulation !== "real" ? " · Systempräferenz simuliert" : ""}.</p>
    <div className="dark-mode-dashboard" data-lesson-theme={theme} style={{ ...style, colorScheme: theme }}>
      <header><p>METRICS / FIKTIVES DASHBOARD</p><h3>Überblick behalten.</h3><p>Vier Beispielwochen · alle Werte sind erfunden.</p></header>
      <div className="dark-mode-nav" role="group" aria-label="Dashboard-Ansichten">{["Übersicht", "Analyse", "Berichte"].map(item => <button key={item} type="button" aria-pressed={view === item} aria-controls={`${id}-view`} onClick={() => setView(item)}>{view === item && <span aria-hidden="true">✓ </span>}{item}</button>)}</div>
      <p id={`${id}-view`} className="dark-mode-view" aria-live="polite">{view === "Übersicht" ? "Übersicht: Kennzahlen, Budgethinweis und Kanalvergleich." : view === "Analyse" ? "Analyse: Suche erreicht im Beispiel in KW 4 den höchsten Wert mit 49 Tsd. Besuchen. Vergleiche die sechs Reihen und ihre Datentabelle unten." : "Berichte: Die vier Wochenwerte aller sechs Kanäle stehen in der aufklappbaren Datentabelle. Es wird kein Export vorgetäuscht."}</p>
      <div className="dark-mode-kpis">{[["Besuche", "750.000", "in vier Beispielwochen"], ["Registrierungen", "8.540", "in vier Beispielwochen"], ["Abschlussrate", "1,14 %", "Registrierungen / Besuche"], ["Kosten", "12.400 €", "Budget: 10.000 €"]].map(([label, value, note]) => <div className="dark-mode-kpi" key={label}><p>{label}</p><strong>{value}</strong><small>{note}</small></div>)}</div>
      <aside className="dark-mode-warning" aria-labelledby={`${id}-warning`}><strong id={`${id}-warning`}>⚠ Budget überschritten</strong><p>Die Kosten liegen 2.400 € über dem angenommenen Monatsbudget. Prüfe die Ausgaben, bevor du weitere Maßnahmen planst.</p><button type="button" aria-expanded={measures} aria-controls={`${id}-measures`} onClick={() => setMeasures(!measures)}>{measures ? "Prüfschritte ausblenden" : "Prüfschritte ansehen"}</button><div id={`${id}-measures`} hidden={!measures}><p>Eigener Vorschlag: Kostenquellen prüfen, Buchungszeitraum abgleichen und Budgetverantwortliche einbeziehen. Dieses Beispiel löst keine Zahlung oder Änderung aus.</p></div></aside>
      <section className="dark-mode-analysis" aria-labelledby={`${id}-chart-title`}><h4 id={`${id}-chart-title`}>Sechs Kanäle, ein Vergleich.</h4><p>Besuche in Tsd. · KW 1–4 · synthetische Werte</p>
        <div className="dark-mode-plot" tabIndex={0} role="region" aria-label="Scrollbares Diagramm"><svg viewBox="0 0 600 300" role="img" aria-labelledby={`${id}-svg-title ${id}-svg-desc`}><title id={`${id}-svg-title`}>Besuche nach Kanal in vier Wochen</title><desc id={`${id}-svg-desc`}>Sechs Reihen mit unterschiedlichen Linienmustern und Symbolen. Die exakten Werte stehen in der Datentabelle direkt darunter. Suche steigt von 42 auf 49 Tsd., Direkt von 25 auf 40 Tsd.</desc>{[0, 25, 50].map(value => <g key={value}><line x1="48" x2="540" y1={250-value*4} y2={250-value*4} className="dark-mode-grid" /><text x="6" y={255-value*4}>{value}</text></g>)}{[1,2,3,4].map((week,index) => <text key={week} x={48+index*160} y="285">KW {week}</text>)}{lessonChartSeries.map(series => <g key={series.name} style={{ color: series[theme] }}><polyline points={series.values.map((value,index) => `${48+index*160},${250-value*4}`).join(" ")} fill="none" stroke="currentColor" strokeWidth="3" strokeDasharray={series.dash} />{series.values.map((value,index) => <text className="dark-mode-marker" key={index} x={48+index*160} y={255-value*4} textAnchor="middle">{series.symbol}</text>)}</g>)}</svg></div>
        <ul className="dark-mode-legend">{lessonChartSeries.map(series => <li key={series.name}><svg width="40" height="20" viewBox="0 0 40 20" aria-hidden="true" style={{color:series[theme]}}><line x1="0" x2="40" y1="10" y2="10" stroke="currentColor" strokeWidth="2" strokeDasharray={series.dash}/><text x="20" y="15" textAnchor="middle" fill="currentColor">{series.symbol}</text></svg><span>{series.name}</span></li>)}</ul>
        <details className="dark-mode-data"><summary>Alle Diagrammwerte als Tabelle</summary><div tabIndex={0} role="region" aria-label="Scrollbare Kanalwerte"><table><caption>Besuche in Tausend, fiktive Beispieldaten</caption><thead><tr><th scope="col">Kanal</th>{[1,2,3,4].map(week=><th scope="col" key={week}>KW {week}</th>)}</tr></thead><tbody>{lessonChartSeries.map(series=><tr key={series.name}><th scope="row">{series.symbol} {series.name}</th>{series.values.map((value,index)=><td key={index}>{value}</td>)}</tr>)}</tbody></table></div></details>
      </section>
    </div>
    <details className="lesson-analysis-details"><summary>Aktuelle Tokens und berechnete Kontraste</summary><div><p>Deckende sRGB-Farben der aktiven Variante. Die Bewertung nutzt den ungerundeten Wert; die Anzeige hat zwei Nachkommastellen. Ein bestandenes Paar ist kein vollständiger Accessibility-Nachweis.</p><dl className="dark-mode-token-list">{Object.entries(tokens).map(([name,value])=><div key={name}><dt>{name}</dt><dd><span style={{background:value}} aria-hidden="true"/>{value}</dd></div>)}</dl><ul className="dark-mode-checks">{checks.map(check=><li key={check.label}><strong>{check.label}</strong><span>{check.ratio.toLocaleString("de-DE",{minimumFractionDigits:2,maximumFractionDigits:2})}:1 · Mindestwert {check.minimum.toLocaleString("de-DE")}:1 · {check.ratio >= check.minimum ? "erfüllt" : "nicht erfüllt"}</span></li>)}</ul><p>Die zarte <code>border</code>-Kontur ordnet dekorative Karten. Identifizierbare Controls erhalten die stärkere <code>control</code>-Kontur. Die sechs Datenreihen werden zusätzlich durch Muster, Symbole und Tabellenwerte unterschieden.</p></div></details>
  </div>;
}

coaching-dark-mode.css

.dark-mode-lesson .lesson-header h1 { overflow-wrap: anywhere; hyphens: auto; }
.dark-mode-lab { margin: 28px 0; min-width: 0; }
.dark-mode-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.dark-mode-controls label { display: grid; gap: 8px; font-size: 14px; font-weight: 600; min-width: 0; }
.dark-mode-controls select { min-height: 44px; width: 100%; padding: 10px; border: 1px solid #64748b; border-radius: 8px; background: #fff; color: #0f172a; font: inherit; }
.dark-mode-lab .dark-mode-resolution { font-size: 14px; margin: 16px 0; }
.dark-mode-dashboard { padding: 24px; background: var(--dm-background); color: var(--dm-primary); border: 1px solid var(--dm-border); border-radius: 16px; min-width: 0; }
.dark-mode-dashboard header h3 { margin: 8px 0; color: var(--dm-primary); font-size: 30px; line-height: 1.3; }
.dark-mode-dashboard header p { font-size: 13px; color: var(--dm-secondary); margin: 0 0 12px; }
.dark-mode-dashboard button { min-height: 44px; padding: 10px 14px; border: 1px solid var(--dm-control); border-radius: 8px; background: var(--dm-surface); color: var(--dm-primary); font: inherit; font-size: 14px; cursor: pointer; overflow-wrap: anywhere; }
.dark-mode-nav { display: flex; flex-wrap: wrap; gap: 10px; margin: 24px 0 12px; }
.dark-mode-nav button[aria-pressed="true"] { background: var(--dm-accent); color: var(--dm-onAccent); border-color: var(--dm-accent); font-weight: 700; }
.dark-mode-dashboard button:hover { text-decoration: underline; text-underline-offset: 4px; }
.dark-mode-dashboard :is(button,summary,[tabindex]):focus-visible { outline: 3px solid var(--dm-focus); outline-offset: 3px; }
.dark-mode-controls select:focus-visible { outline: 3px solid #4338ca; outline-offset: 3px; }
.dark-mode-dashboard .dark-mode-view { font-size: 14px; min-height: 52px; line-height: 1.75; color: var(--dm-secondary); }
.dark-mode-kpis { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 14px; margin: 20px 0; }
.dark-mode-kpi { padding: 20px; background: var(--dm-surface); border: 1px solid var(--dm-border); border-radius: 12px; min-width: 0; }
.dark-mode-kpi p { font-size: 14px; color: var(--dm-secondary); margin: 0 0 8px; }
.dark-mode-kpi strong { display: block; font-size: clamp(24px,3vw,32px); line-height: 1.3; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.dark-mode-kpi small { display: block; font-size: 12px; color: var(--dm-secondary); margin-top: 8px; }
.dark-mode-warning { padding: 20px; margin: 20px 0; border-radius: 12px; border-left: 4px solid var(--dm-danger); background: var(--dm-dangerSurface); color: var(--dm-danger); }
.dark-mode-warning strong { font-size: 17px; }
.dark-mode-warning p { color: var(--dm-danger); font-size: 14px; line-height: 1.8; }
.dark-mode-analysis { padding: 20px; border: 1px solid var(--dm-border); border-radius: 12px; background: var(--dm-raised); min-width: 0; }
.dark-mode-analysis h4 { font-size: 21px; line-height: 1.4; margin: 0; color: var(--dm-primary); }
.dark-mode-analysis p { font-size: 13px; color: var(--dm-secondary); }
.dark-mode-plot { overflow-x: auto; padding: 5px; }
.dark-mode-plot > svg { width: 100%; min-width: 480px; height: auto; display: block; }
.dark-mode-plot text { fill: var(--dm-secondary); font: 16px var(--font-sans),sans-serif; }
.dark-mode-plot .dark-mode-marker { fill: currentColor; font-size: 18px; }
.dark-mode-grid { stroke: var(--dm-border); }
.dark-mode-analysis .dark-mode-legend { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0; }
.dark-mode-legend li { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--dm-primary); min-width: 0; }
.dark-mode-legend svg { flex-shrink: 0; }
.dark-mode-data { border-top: 1px solid var(--dm-border); margin-top: 20px; }
.dark-mode-data summary { min-height: 44px; padding: 14px 0; cursor: pointer; font-size: 14px; }
.dark-mode-data > div { overflow-x: auto; padding: 5px; }
.dark-mode-data table { width: 100%; font-size: 13px; border-collapse: collapse; color: var(--dm-primary); }
.dark-mode-data caption { text-align: left; margin-bottom: 12px; color: var(--dm-secondary); }
.dark-mode-data :is(th,td) { text-align: left; padding: 12px; border-bottom: 1px solid var(--dm-border); white-space: nowrap; }
.dark-mode-token-list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 20px 0; }
.dark-mode-token-list > div { min-width: 0; }
.dark-mode-token-list dt { font-size: 13px; font-weight: 600; }
.dark-mode-token-list dd { display: flex; align-items: center; gap: 8px; font: 13px monospace; margin: 5px 0 0; }
.dark-mode-token-list dd span { width: 18px; height: 18px; border: 1px solid #64748b; border-radius: 3px; }
.dark-mode-lab .dark-mode-checks { list-style: none; padding: 0; margin: 20px 0; }
.dark-mode-checks li { display: grid; gap: 4px; padding: 12px 0; border-bottom: 1px solid #d9dfe7; font-size: 14px; }
.dark-mode-checks span { color: #475569; }
@media(max-width:600px) { .dark-mode-controls { grid-template-columns: 1fr; } .dark-mode-dashboard { padding: 16px; } .dark-mode-kpis { grid-template-columns: 1fr; } .dark-mode-kpi,.dark-mode-analysis,.dark-mode-warning { padding: 16px; } .dark-mode-analysis .dark-mode-legend { grid-template-columns: 1fr; } .dark-mode-token-list { grid-template-columns: 1fr; } }

05 / ACCESSIBILITY · 1 MINUTE

Mindestkontrast und visuelle Qualität getrennt prüfen.

WCAG 2.2 AA verlangt für normalen Text grundsätzlich mindestens 4,5:1, für großen Text 3:1. „Groß“ bedeutet mindestens 18 pt regulär oder 14 pt fett, entsprechend ungefähr 24 beziehungsweise 18,7 CSS-Pixeln. Es bestehen Ausnahmen, etwa für rein dekorativen Text und inaktive Controls. W3C: Contrast (Minimum).

Für die zur Erkennung von UI-Komponenten und Zuständen nötige visuelle Information sowie wesentliche grafische Informationen gelten grundsätzlich 3:1 gegen benachbarte Farben, mit definierten Ausnahmen. Das ist keine pauschale Vorgabe für jede dekorative Kartenkontur. W3C: Non-text Contrast.

Farbe darf eine Bedeutung nicht allein vermitteln. Darum besitzt die Warnung Text und Symbol; die Diagrammreihen haben neben Farbe unterschiedliche Muster und Marker sowie eine Datentabelle. Kontrast gegen den Hintergrund allein garantiert noch nicht, dass sich sechs Farben voneinander unterscheiden lassen. W3C: Use of Color.

Prüfe beide Themes bei 320 CSS-Pixeln Breite, mit vergrößerter Schrift und mit der Tastatur. Das Diagramm und die Tabelle dürfen auf schmalen Ansichten in ihrem eigenen, benannten Bereich horizontal scrollen. Die restliche Seite soll dabei umbrechen. Ein automatisch bestandener Farbtest ersetzt diese Prüfungen nicht.

06 / REFLEXION · 2 MINUTEN

Welche Hierarchie bleibt erhalten?

Wie trennst du Hintergrund, Karte und Analysepanel?

Wählst du hauptsächlich Helligkeit, Konturen oder Schatten? Begründe die Kombination für dieses Dashboard.

Mögliche Antwort zur Oberflächenhierarchie

Ich beginne mit moderat abgestuften Helligkeiten, klaren Abständen und Überschriften. Eine zurückhaltende Kontur hilft beim Gruppieren; Schatten wären höchstens eine Ergänzung. Das Analysepanel erhält eine eigene Fläche, ohne mit dem Warnhinweis um Aufmerksamkeit zu konkurrieren.

Ich prüfe, ob die wichtigen Informationen zuerst erkennbar sind und ob Sekundärtexte auf jeder tatsächlich verwendeten Fläche lesbar bleiben. Eine pauschale Regel wie „jede höhere Ebene muss deutlich heller sein“ wäre für alle Komponenten und Aufgaben zu grob.

Das System steht auf Dark, die App ausdrücklich auf Light.

Welche Einstellung gewinnt, und wie kann die Person wieder der Systempräferenz folgen?

Mögliche Antwort zur Theme-Auswahl

Die ausdrücklich gewählte App-Einstellung Light hat Vorrang. Ich biete drei klar benannte Optionen: Light, Dark und System. Bei System folgt die App wieder der Betriebssystempräferenz und reagiert auf deren Änderungen.

Ein Schalter mit nur zwei Zuständen könnte unklar lassen, ob er eine feste Wahl oder die aktuelle Systemlage zeigt. Die Auswahl sollte verständlich bleiben, auch wenn sich das System später ändert. Ob sie dauerhaft gespeichert wird, entscheidet das Produkt; das Labor speichert sie bewusst nicht.

Dein Selbstcheck

  • Haben Flächen, Texte, Aktion und Warnung eindeutig benannte Rollen?
  • Prüfst du kleine Sekundärtexte auf ihrer wirklichen Hintergrundfläche?
  • Ist die Beschriftung auf der Akzentfarbe in beiden Themes lesbar?
  • Bleiben Auswahl, Hover und Fokus auch gleichzeitig verständlich?
  • Sind sechs Datenreihen ohne reine Farberkennung zuordenbar?

DEIN MERKSATZ

Dark Mode ist kein Farbfilter.

Er ist eine alternative visuelle Hierarchie mit denselben funktionalen Anforderungen. Die Herausforderung liegt in der Abstimmung von Oberflächen, Text, Statusfarben und Interaktionszuständen zu einem konsistenten System.