Nutzerprofile jenseits von Hell und Dunkel
Ein binärer Hell/Dunkel-Schalter deckt nur eine einzige Präferenz-Achse ab. Nutzer mit Sehschwäche, Bewegungssensibilität oder dem Wunsch nach mehr Übersicht auf kleinen Bildschirmen brauchen mehr Auswahl. Tailwind CSS Theme-Presets kombinieren Kontrast, Dichte und Bewegung zu benannten Profilen, die sich über Alpine.js auswählen und serverseitig persistieren lassen.
Inhaltsverzeichnis
- 1. Warum Hell und Dunkel allein nicht reicht
- 2. Die drei Achsen: Farbschema, Dichte und Bewegung
- 3. Benannte Theme-Presets bauen: Comfort, Fokus, Kontrast, Kompakt
- 4. Preset-Token mit @theme und CSS Custom Properties
- 5. Preset-Auswahl-UI mit Alpine.js
- 6. Persistenz: localStorage, Cookies und SSR-Sync
- 7. Systempräferenzen erkennen: prefers-contrast und prefers-reduced-motion
- 8. FOUC vermeiden: Bootstrap-Script vor dem ersten Paint
- 9. Theme-Presets im Vergleich zu einem einfachen Dark-Mode-Schalter
- 10. Zusammenfassung
- 11. FAQ
1. Warum Hell und Dunkel allein nicht reicht
Die meisten Tailwind-Projekte implementieren Theming als einzige binäre Entscheidung: hell oder dunkel. Das löst ein reales Problem, deckt aber nur eine von mehreren Präferenz-Achsen ab, die Nutzer tatsächlich haben. Jemand mit eingeschränktem Sehvermögen braucht vielleicht mehr Kontrast, unabhängig davon, ob das Farbschema hell oder dunkel ist. Jemand mit vestibulären Beschwerden möchte Animationen minimieren, ganz unabhängig vom Farbschema. Und jemand, der einen Backoffice-Bildschirm mit vielen Tabellen nutzt, profitiert von einer kompakteren Darstellung, die mit Hell/Dunkel überhaupt nichts zu tun hat. Theme-Presets lösen genau dieses Problem, indem sie mehrere unabhängige Achsen zu sinnvollen, benannten Kombinationen zusammenfassen.
Der Denkfehler bei vielen Theme-Implementierungen ist, alle Präferenzen in einen einzigen Schalter zu pressen. Ein Projekt, das nur dark: kennt, muss für jede neue Anforderung einen neuen bedingten Zweig einbauen: dark:contrast:, compact:dark: und so weiter, bis die Variantenkombinationen unübersichtlich werden. Theme-Presets als eigenständiges Konzept trennen die Achsen sauber auf CSS-Ebene und fassen sie erst auf UI-Ebene zu benutzerfreundlichen Profilen zusammen. Das hält die zugrunde liegenden Custom Properties einfach, während die Auswahl für den Nutzer trotzdem übersichtlich bleibt.
2. Die drei Achsen: Farbschema, Dichte und Bewegung
Ein robustes System von Theme-Presets beginnt mit der Definition unabhängiger Achsen, bevor überhaupt eine einzige Kombination benannt wird. Die erste Achse ist das Farbschema: hell, dunkel oder ein hochkontrastiges Derivat von beiden. Die zweite Achse ist die Dichte: komfortabel mit großzügigem Zeilenabstand und Innenabständen, oder kompakt mit reduzierten Abständen für datenintensive Ansichten. Die dritte Achse ist die Bewegung: normal mit vollen Übergängen und Animationen, oder reduziert, bei dem Transitions auf ein Minimum oder ganz auf sofortige Zustandswechsel reduziert werden.
Jede Achse wird als eigene Gruppe von CSS Custom Properties modelliert, die unabhängig voneinander gesetzt werden können. Das ist der entscheidende Unterschied zu einem klassischen Dark-Mode-System: Statt einer einzigen Klasse .dark gibt es drei unabhängige Attribute, data-scheme, data-density und data-motion, die theoretisch in 2 mal 2 mal 2 Kombinationen auftreten können. In der Praxis reduziert man diese Kombinatorik, indem man nicht jede einzelne Kombination als UI-Option anbietet, sondern nur sinnvolle, benannte Theme-Presets, die die häufigsten Bedürfnisse abdecken.
3. Benannte Theme-Presets bauen: Comfort, Fokus, Kontrast, Kompakt
Statt Nutzern drei separate Schalter für drei Achsen zu zeigen, was kognitiv überfordernd wirkt, definiert man vier bis fünf benannte Theme-Presets, die typische Kombinationen abbilden. Comfort ist die Standardeinstellung mit normalem Kontrast, komfortabler Dichte und vollen Animationen. Fokus reduziert visuelles Rauschen mit gedämpfteren Akzentfarben und weniger Animation, gedacht für lange Arbeitssitzungen. Kontrast erhöht Farbkontraste über WCAG AAA hinaus und wird typischerweise mit dunklem oder hellem Hintergrund kombiniert, je nach Systemeinstellung. Kompakt reduziert Innenabstände und Zeilenhöhen für Tabellen- und Dashboard-lastige Ansichten.
Der Clou an benannten Theme-Presets ist, dass jedes Preset eine feste Kombination aus Werten für alle drei Achsen vorgibt, aber der Nutzer trotzdem einzelne Achsen nachträglich überschreiben kann. Wählt jemand das Preset Kontrast, aber möchte trotzdem reduzierte Bewegung, kann er das als zusätzliche Einstellung übersteuern. Diese Zwei-Ebenen-Architektur, benannte Presets als Schnellauswahl plus granulare Overrides für Detailkontrolle, ist der Unterschied zwischen einem echten Präferenzsystem und einem einfachen Theme-Umschalter.
/* presets.css — token definitions for named Theme-Presets, three independent axes */
@import "tailwindcss";
/* === Axis 1: color scheme (light / dark / high-contrast) === */
:root, [data-scheme="light"] {
--color-surface: #ffffff;
--color-text: #0f172a;
--color-border: #e2e8f0;
--color-accent: #0369a1;
}
[data-scheme="dark"] {
--color-surface: #0f172a;
--color-text: #f1f5f9;
--color-border: #334155;
--color-accent: #38bdf8;
}
[data-scheme="high-contrast"] {
--color-surface: #000000;
--color-text: #ffffff;
--color-border: #ffffff;
--color-accent: #ffff00;
}
/* === Axis 2: density (comfortable / compact) === */
[data-density="comfortable"], :root {
--spacing-row: 1rem;
--spacing-cell: 0.75rem;
--line-height-base: 1.6;
}
[data-density="compact"] {
--spacing-row: 0.375rem;
--spacing-cell: 0.375rem;
--line-height-base: 1.3;
}
/* === Axis 3: motion (normal / reduced) === */
[data-motion="normal"], :root {
--duration-transition: 200ms;
--duration-modal: 300ms;
}
[data-motion="reduced"] {
--duration-transition: 0ms;
--duration-modal: 0ms;
}
@theme {
--color-preset-surface: var(--color-surface);
--color-preset-text: var(--color-text);
--color-preset-accent: var(--color-accent);
--spacing-preset-row: var(--spacing-row);
}
4. Preset-Token mit @theme und CSS Custom Properties
Die Verbindung zwischen den achsenbasierten Custom Properties und den Tailwind-Utilities läuft über die @theme-Direktive von Tailwind CSS v4. Jede Achse definiert ihre Werte in einem eigenen Custom-Property-Namensraum, und @theme referenziert nur die Namen, die tatsächlich als Utility gebraucht werden. Wichtig für ein sauberes Theme-Presets-System: Die Utility-Klassen selbst kennen die Achsen nicht. Eine Klasse wie bg-preset-surface weiß nichts von Dichte oder Bewegung, sie zeigt einfach auf den aktuellen Wert von --color-surface, egal welches Preset aktiv ist.
Diese Entkopplung ist entscheidend für die Wartbarkeit. Wenn ein neues Preset hinzukommt, zum Beispiel ein saisonales Kontrast-Kompakt-Profil für den Weihnachtsverkauf im Backoffice, muss keine einzige Tailwind-Klasse in den Templates angefasst werden. Es reicht, einen neuen data-scheme, data-density oder data-motion Wert zu definieren oder eine neue Kombination aus bestehenden Werten als Preset-Namen zu registrieren. Die Utility-Ebene bleibt stabil, während sich die darunterliegende Token-Ebene beliebig erweitern lässt.
5. Preset-Auswahl-UI mit Alpine.js
Die Benutzeroberfläche für die Auswahl von Theme-Presets sollte als eigenständige Alpine.js-Komponente implementiert werden, die vom Rest der Anwendung entkoppelt ist. Ein zentraler Alpine-Store hält den aktuell aktiven Preset-Namen sowie eventuelle granulare Overrides. Beim Wechsel eines Presets setzt die Komponente die drei Data-Attribute auf dem html-Element in einem einzigen Zug, was CSS Custom Properties sofort neu auflösen lässt, ohne dass ein einziger DOM-Knoten neu gerendert werden muss.
Ein wichtiger Aspekt für die Zugänglichkeit der UI selbst: Die Preset-Auswahl sollte über eine Radiogroup mit korrekten ARIA-Rollen umgesetzt werden, nicht über einfache Buttons ohne semantische Bedeutung. Tastaturnutzer müssen zwischen den Theme-Presets mit Pfeiltasten navigieren können, und Screenreader müssen die aktuell aktive Auswahl klar ansagen. Das ist besonders wichtig, weil ein System, das Barrierefreiheit als Kernfunktion anbietet, in seiner eigenen Bedienoberfläche nicht selbst unzugänglich sein darf.
<!-- Theme-Preset switcher — accessible radiogroup with Alpine.js -->
<div
x-data="{
preset: $store.themePresets.active,
presets: [
{ id: 'comfort', label: 'Comfort' },
{ id: 'focus', label: 'Fokus' },
{ id: 'contrast', label: 'Kontrast' },
{ id: 'compact', label: 'Kompakt' }
],
select(id) {
this.preset = id;
$store.themePresets.apply(id);
}
}"
role="radiogroup"
aria-label="Theme-Preset auswählen"
class="flex flex-wrap gap-2"
>
<template x-for="p in presets" :key="p.id">
<button
type="button"
role="radio"
:aria-checked="preset === p.id"
@click="select(p.id)"
:class="preset === p.id
? 'bg-preset-accent text-white'
: 'bg-white text-slate-700 border border-slate-200'"
class="px-4 py-2 rounded-lg text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2"
x-text="p.label"
></button>
</template>
</div>
6. Persistenz: localStorage, Cookies und SSR-Sync
Ein System von Theme-Presets ist nur so nützlich wie seine Persistenz über Seitenaufrufe hinweg. Für rein clientseitige Anwendungen genügt localStorage. Für serverseitig gerenderte Seiten wie ein Magento-2-Shop mit Hyvä Themes reicht das nicht: Das erste HTML, das der Server ausliefert, muss das richtige Preset bereits kennen, sonst blitzt kurz das Standard-Preset auf, bevor JavaScript umschaltet. Die Lösung ist ein zusätzliches Cookie, das bei jeder Preset-Änderung parallel zu localStorage gesetzt wird und vom Server bei jedem Request gelesen werden kann.
Für angemeldete Kunden lohnt sich zusätzlich, das aktive Preset als Kundenattribut in Magento zu speichern, damit die Präferenz geräteübergreifend erhalten bleibt und nicht nur an ein einzelnes Cookie im Browser gebunden ist. Der Ablauf: Beim Login wird das gespeicherte Kundenattribut gelesen und als Cookie gesetzt, danach übernimmt das Cookie die laufende Synchronisation. Diese Kombination aus Cookie für schnellen SSR-Zugriff und Kundenattribut für Persistenz über Geräte hinweg deckt praktisch alle Szenarien für Theme-Presets in einem Magento-Kontext ab.
// theme-presets.js — persistence layer with cookie sync for SSR
'use strict';
const COOKIE_NAME = 'theme_preset';
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // one year
/**
* Persist the active preset in both localStorage and a cookie.
* The cookie is what makes server-side rendering aware of the preset
* on the very first request, avoiding a flash of the default preset.
* @param {string} presetId - e.g. 'comfort', 'focus', 'contrast', 'compact'
*/
function persistPreset(presetId) {
localStorage.setItem(COOKIE_NAME, presetId);
document.cookie = `${COOKIE_NAME}=${presetId}; max-age=${COOKIE_MAX_AGE}; path=/; samesite=lax`;
}
/**
* Read the active preset, preferring localStorage over the cookie
* since it is updated more frequently on the client.
* @returns {string} The active preset id, defaults to 'comfort'
*/
function readPreset() {
return localStorage.getItem(COOKIE_NAME) || readCookie(COOKIE_NAME) || 'comfort';
}
function readCookie(name) {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
document.addEventListener('alpine:init', () => {
Alpine.store('themePresets', {
active: readPreset(),
apply(presetId) {
this.active = presetId;
persistPreset(presetId);
document.documentElement.setAttribute('data-preset', presetId);
}
});
});
7. Systempräferenzen erkennen: prefers-contrast und prefers-reduced-motion
Bevor ein Nutzer überhaupt eine bewusste Auswahl trifft, sollte ein Theme-Presets-System versuchen, sinnvolle Standardwerte aus den Betriebssystem-Einstellungen abzuleiten. Die CSS-Media-Query prefers-contrast: more signalisiert, dass der Nutzer bereits auf Betriebssystemebene erhöhten Kontrast eingestellt hat. Die Media-Query prefers-reduced-motion: reduce signalisiert eine Präferenz für reduzierte Bewegung, oft aus medizinischen Gründen wie vestibulären Störungen. Beide Signale sollten beim ersten Besuch die Preset-Auswahl vorbelegen, aber niemals eine spätere manuelle Auswahl des Nutzers überschreiben.
Die richtige Priorisierung lautet: Ein gespeichertes Cookie oder Kundenattribut hat immer Vorrang, weil es eine bewusste Entscheidung repräsentiert. Fehlt eine gespeicherte Präferenz, werden die System-Media-Queries ausgewertet, um ein passendes Preset vorzuschlagen. Erst wenn auch das keine klare Präferenz ergibt, greift das Standard-Preset Comfort. Diese Reihenfolge stellt sicher, dass Theme-Presets sich wie ein respektvolles System verhalten, das Nutzerentscheidungen niemals stillschweigend überschreibt.
8. FOUC vermeiden: Bootstrap-Script vor dem ersten Paint
Das größte technische Risiko bei serverseitig gerenderten Seiten mit clientseitiger Preset-Logik ist ein kurzes Aufblitzen des falschen Themes, bekannt als Flash of Unstyled Content oder in diesem Fall eher Flash of Incorrect Theme. Die Lösung ist ein winziges, synchron ausgeführtes Inline-Script direkt im head, noch vor dem restlichen CSS und noch vor dem großen Alpine.js-Bundle. Dieses Script liest Cookie oder localStorage und setzt die Data-Attribute sofort, bevor der Browser überhaupt etwas rendert.
Wichtig ist, dieses Bootstrap-Script so klein und so schnell wie möglich zu halten. Es darf keine Netzwerkanfragen auslösen und keine schweren Berechnungen durchführen, denn jede Millisekunde Verzögerung an dieser Stelle blockiert das Rendering der gesamten Seite. In Hyvä-Themes wird ein solches Script direkt im Layout-XML des head-Blocks registriert und über $hyvaCsp->registerInlineScript() für die Content-Security-Policy freigegeben, damit es trotz strikter CSP-Regeln ausgeführt werden darf.
<!-- Bootstrap script in <head>, before any stylesheet or Alpine bundle -->
<script>
(function () {
function readCookie(name) {
var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return m ? decodeURIComponent(m[1]) : null;
}
// Synchronous, tiny, no network calls — prevents Flash of Incorrect Theme
var preset = localStorage.getItem('theme_preset') || readCookie('theme_preset') || 'comfort';
document.documentElement.setAttribute('data-preset', preset);
})();
</script>
9. Theme-Presets im Vergleich zu einem einfachen Dark-Mode-Schalter
Der Unterschied zwischen einem klassischen Dark-Mode-Schalter und einem vollständigen System aus Theme-Presets zeigt sich am deutlichsten in der Praxis, wenn reale Nutzeranforderungen zusammenkommen. Die folgende Tabelle stellt beide Ansätze entlang der wichtigsten Kriterien gegenüber.
| Kriterium | Einfacher Dark-Mode-Schalter | Theme-Presets |
|---|---|---|
| Abgedeckte Achsen | Nur Farbschema | Farbschema, Dichte, Bewegung, erweiterbar |
| Kontrast-Bedürfnisse | Nicht abgedeckt | Eigenes Kontrast-Preset |
| Bewegungssensibilität | Nicht abgedeckt | prefers-reduced-motion integriert |
| Granulare Overrides | Nicht vorgesehen | Preset plus Einzel-Override möglich |
| Erweiterbarkeit | Neue Anforderung sprengt Binärlogik | Neue Achse oder neues Preset ohne Refactoring |
In der Praxis zeigt sich, dass Projekte, die früh auf mehrere unabhängige Achsen setzen, deutlich seltener größere Refactorings brauchen, wenn neue Barrierefreiheits- oder Komfort-Anforderungen dazukommen. Ein einfacher Dark-Mode-Schalter fühlt sich am Anfang einfacher an, wird aber bei jeder neuen Anforderung zur Bremse. Theme-Presets investieren früh etwas mehr Architekturaufwand und zahlen sich über die Lebensdauer eines Projekts konsequent aus.