Tailwind v4 Design Tokens: Farben, Dark Mode, Kontrast

Tailwind CSS v4 hat die Konfiguration aus der tailwind.config.js ins CSS verlegt, und viele Guides hören beim ersten Schritt auf: ein paar Markenfarben in @theme packen und das Ganze Tokens nennen. Eine Komponentenbibliothek braucht mehr als eine Palette. Sie braucht Tailwind v4 Design Tokens für jede Entscheidung, die eine Komponente trifft: welche Fläche vor welcher liegt, wie blass sekundärer Text werden darf, was eine Warnung als Füllung und was sie als Text nutzt, wie rund ein 32px-Button neben einem 48px-Button ist, wie ein Popover auf einer schwarzen Seite einen Schatten wirft und auf welcher Ebene sich ein Select innerhalb eines Dialogs öffnet. Dieser Guide zeigt das Token-System, das wir in Wingo UI nutzen, einer Komponentenbibliothek für React 19 und Tailwind CSS v4, mit dem echten CSS und den Kontrastwerten hinter jeder Entscheidung. Wir bauen Wingo UI, sind also nicht neutral; Fakten von außen sind mit ihrer Quelle verlinkt und datiert.
Was sind Design Tokens in Tailwind v4?
Design Tokens sind in Tailwind v4 CSS Custom Properties. Die Tailwind v4 Theme-Variablen, die du in einem @theme-Block deklarierst, haben zwei Aufgaben zugleich: Sie sind CSS-Variablen, die du mit var() lesen kannst, und ihr Namespace entscheidet, welche Utilities Tailwind erzeugt. --color-surface erzeugt bg-surface, text-surface und border-surface; --shadow-raised erzeugt shadow-raised. Tailwind CSS v4.0 hat diese CSS-first-Konfiguration am 22. Januar 2025 ausgeliefert (Release-Post (opens in a new tab)), und die Doku zu Theme-Variablen (opens in a new tab) listet die Namespaces. Wir nutzen Stand Oktober 2026 Tailwind 4.3.3.
Die Namespaces, die eine Komponentenbibliothek am häufigsten nutzt:
--z-index-* und --transition-duration-* stehen Stand Oktober 2026 nicht in der Tabelle der Doku, Tailwind 4.3 kompiliert sie aber: Unser CSS enthält .z-modal { z-index: var(--z-modal) }.
Wie solltest du Tailwind v4 Design Tokens in Schichten aufbauen?
Nutz drei Schichten. Rohwerte, die sich zwischen den Themes ändern, stehen in :root und .dark. Ein Mapping in @theme inline macht daraus Utilities. Komponenten beziehen ihre Farben dann aus ein paar Laufzeit-Variablen, die ein Prop setzt, sodass ein tone oder eine eigene color nie eine eigene Klasse braucht.
Hier ein gekürzter, aber lauffähiger Auszug aus unserer app/globals.css, mit den echten Werten:
Die Success-Füllung und ihr weißer Text sind in beiden Themes gleich (5.02:1), deshalb deklariert dieser Auszug sie nur in :root, und .dark erbt sie. Alles, was sich ändert, wird unter .dark neu deklariert, und eine Komponente nutzt überall dieselben Klassen:
Die Rohnamen folgen bewusst der Konvention von shadcn/ui: --background, --card, --popover und --muted-foreground sind shadcn-Namen, und die, die shadcn nicht hat (--surface, --canvas, die --success-*-Familie), folgen demselben Muster aus name und name-foreground. Ein Projekt, das schon shadcn-Tokens hat, kann unsere übernehmen, ohne etwas umzubenennen.
Wann brauchst du Tailwind v4 @theme inline?
Nimm @theme inline immer dann, wenn eine Theme-Variable auf eine andere Variable zeigt, und das gilt für jedes Token, das sich mit dem Theme ändert. Die Tailwind-Doku (opens in a new tab) sagt es direkt: Mit inline nutzt die Utility-Klasse den Wert der Theme-Variable, statt auf die Theme-Variable selbst zu verweisen. So sieht das im kompilierten CSS aus:
Der Unterschied zeigt sich unterhalb des Root-Elements. Eine Custom Property erbt ihren berechneten Wert, und var() wird dort eingesetzt, wo die Property deklariert ist. In der zweiten Version wird --color-surface einmal aufgelöst, auf :root, und jedes Element erbt dieses Ergebnis. Eine .dark-Klasse auf <html> funktioniert trotzdem, denn <html> ist das Root-Element. Eine dunkle Section in einer hellen Seite (<section className="dark">), ein dunkler Vorschaubereich oder ein data-theme-Attribut auf einem Container funktionieren nicht: Die Section erbt den hellen Wert, den :root schon aufgelöst hat. Mit inline liest bg-surface var(--surface) auf dem Element selbst und übernimmt so den nächstgelegenen Override. Die Tailwind-Doku erklärt die Option mit einem ganz ähnlichen Beispiel aus Eltern- und Kindelement.
Einen Haken hat inline noch: Wenn du selbst var(--color-surface) schreibst, in einem Arbitrary Value wie bg-[var(--color-surface)] oder in normalem CSS, gibt Tailwind 4.3 dafür --color-surface: var(--surface) auf :root aus, und diese Referenz wird wieder auf dem Root-Element aufgelöst. Lies in Komponenten deshalb die Rohvariable: bg-(--surface) oder var(--surface).
Zwei weitere Regeln aus der Doku: Theme-Variablen müssen auf oberster Ebene stehen, nie unter einem Selektor oder einer Media Query, also landen Werte pro Theme ohnehin außerhalb von @theme; und Tailwind gibt nur die Theme-Variablen aus, die dein Code nutzt, außer du schreibst @theme static. Die Rohvariablen in :root werden immer ausgeliefert, was hilft, wenn eine Chart-Bibliothek ein Token mit getComputedStyle liest.
Welche semantischen Farb-Tokens braucht eine Komponentenbibliothek?
Benenn Farben nach ihrer Rolle, nie nach dem Farbton. Eine Komponentenbibliothek braucht Flächen in der Reihenfolge, in der sie sich stapeln, Textstufen mit einer Kontrast-Untergrenze, transparente Tints für Hover und Füllungen, einen Rahmen und einen Fokusring sowie Status-Tones. Semantische Farb-Tokens sorgen dafür, dass Komponenten nie bg-gray-100 oder text-blue-600 enthalten, und ein Theme kann jeden Wert ändern, ohne eine Komponente anzufassen.
Flächen, von hinten nach vorn
Im Dark Mode werden die Flächen heller, je weiter sie nach vorn kommen: #000, #121212, #1A1A1A, #262626. Im Light Mode sind die meisten weiß, also übernimmt der Schatten die Trennung. canvas gibt es, weil ein Dashboard auf einer weißen Seite nichts hat, wovon sich weiße Cards abheben könnten; eine kühle graue Seite sorgt dafür, dass jedes bg-surface-Panel als Card wirkt.
Textstufen
text-muted-foreground ist die Untergrenze für alles, was Menschen lesen müssen. Die deaktivierte Stufe hat kein Kontrastziel, weil WCAG 2.2 Erfolgskriterium 1.4.3 (opens in a new tab) Text in inaktiven Controls ausnimmt, und deshalb wird sie nie für etwas anderes genutzt.
Tints statt grauer Füllungen
Hover-States, Feldfüllungen, Chips, Tracks und Skeletons nutzen transparenten Foreground: bg-foreground/5, /7, /10 und /15, dazu border-foreground/10 für Haarlinien. Tailwind 4.3 kompiliert den Opacity-Modifier zu color-mix(), mit der reinen Farbe als Fallback:
Ein Tint bezieht sich auf das, was hinter ihm liegt, also rückt er auf jeder Fläche und in beiden Themes eine Stufe in Richtung Textfarbe. Eine feste Füllung kann das nicht. Unser dunkles --muted ist #171717: heller als die schwarze Seite und dunkler als die #1A1A1A-Surface, dasselbe Feld würde also auf der einen erhaben und auf der anderen vertieft wirken. Der Fallback in voller Stärke erscheint nur in Browsern ohne color-mix(), und die liegen alle unter Tailwinds Browser-Baseline (opens in a new tab) von Chrome 111, Safari 16.4 und Firefox 128.
secondary und accent von shadcn/ui bleiben in unserer Datei, damit portierte Komponenten weiter funktionieren, aber unser --accent ist ein oranger Tint, und neue Komponenten nutzen keins von beiden.
Wie funktionieren Statusfarben ohne eine Klasse pro Farbe?
Gib jedem Status-Tone vier Tokens: eine volle Füllung, den Text auf dieser Füllung, eine weiche getönte Fläche und Tone-Text, der auf der weichen Fläche und auf jeder neutralen Fläche lesbar ist. Dann lass Komponenten ihre Farben aus fünf generischen Variablen beziehen, die eine einzige Klasse setzt. Das ist die ganze Idee hinter dem kostenlosen Tones-Utility.
Die vier Tokens pro Tone, für Brand, Success, Warning, Destructive und Info:
Die Trennung zwischen Füllung und Tone-Text ist die Regel, die man am leichtesten überspringt, und genau dort bricht der Dark Mode. Füllungen sind darauf abgestimmt, weißen Text zu tragen, und als Text taugen sie wenig. text-destructive (#DC2626) auf unserem dunklen Popover kommt auf 3.13:1; text-destructive-soft-foreground (#FF8A80) auf derselben Fläche auf 6.63:1. Die Warning-Füllung ist Amber (#F5A524) mit 2.04:1 gegen Weiß, also trägt sie dunklen Text (#2B1700, 8.40:1) und wird nie als Text auf einer neutralen Fläche genutzt.
Komponenten-Variablen
Eine Komponente mit einem tone-Prop mappt Tokens nicht selbst. toneClasses[tone] setzt fünf Variablen, und jede Variante färbt sich damit ein:
Der neutrale Tone mappt --tone auf --primary (schwarz im Light, fast weiß im Dark) und baut seine weiche Fläche aus color-mix(in oklab, var(--foreground) 7%, transparent), deshalb funktioniert ein neutraler Soft-Button auf jeder Fläche. Ein Badge nach diesem Muster:
bg-(--tone-soft) ist die Kurzschreibweise von Tailwind v4 für bg-[var(--tone-soft)]. Das kostenlose Badge ist die vollständige Version dieses Musters, mit den Varianten Solid, Soft und Outline. colorVars(color) baut dieselben fünf Variablen aus einer beliebigen CSS-Farbe als Inline-Style. Der Wert für Text auf der Füllung nutzt Relative Color Syntax, oklch(from <color> clamp(0, (0.7 - l) * 1000, 1) 0 0): weiß, wenn die OKLCH-Helligkeit der Farbe unter 0.7 liegt, schwarz darüber. Stand Oktober 2026 listet caniuse (opens in a new tab) volle Unterstützung für Relative Color Syntax ab Chrome 131, Safari 18 und Firefox 133 und davor nur teilweise Unterstützung; volle Unterstützung kam also später als Tailwinds eigene Baseline. Wo ein Browser den Wert nicht berechnen kann, ist die Deklaration zum Zeitpunkt der Wertberechnung ungültig, und der Text erbt die Farbe seines Elternelements. Wenn du diese Browser unterstützt, erzwing die Textfarbe mit className="[--tone-foreground:white]".
$ npx wingo-ui@latest add tonesDer kostenlose Button ist das Referenzbeispiel für die Nutzung. Seine Varianten-Map enthält überhaupt keine Farbe:
Sechs Tones mal fünf Varianten, plus jede eigene Farbe, aus fünf Zeilen. Hover und Press sind eine Schicht aus currentColor mit 8% und 12% Deckkraft, also funktionieren auch sie für jeden Tone ohne zusätzliche Klassen.
Wie gestaltest du Dark Mode mit Tokens?
Behandle Dark als zweiten Satz Werte für dieselben Rollen, von Hand gewählt, und schalte ihn mit einer Klasse um. Komponenten behalten in beiden Themes dieselben Klassen, also braucht fast nichts in einer Komponente ein dark:-Präfix.
Die Variante: Unsere Tokens nutzen, wie die von shadcn/ui, @custom-variant dark (&:is(.dark *));. Die Tailwind-Doku zum Dark Mode (opens in a new tab) zeigt &:where(.dark, .dark *), das auch das Element selbst trifft, das die Klasse trägt, und keine Spezifität hinzufügt. Mit der Klasse auf <html> verhalten sich beide im normalen Einsatz gleich. Die Klasse selbst kommt von next-themes (opens in a new tab), das sie vor dem ersten Paint schreibt: Pack die App in seinen ThemeProvider mit attribute="class" (die vollständige Provider-Datei steht weiter unten im Abschnitt zu Props) und rendere im Root-Layout <html lang="en" suppressHydrationWarning>, denn next-themes ändert die Klasse, bevor React hydriert:
Zwei Posts vertiefen das Umschalten selbst: Tailwind v4 Dark Mode behandelt die @custom-variant-Zeile, den Toggle und warum dark: sie ignorieren kann, und der Fix für den next-themes Hydration Error entfernt das Aufblitzen des Themes und rendert einen Toggle, der schon beim ersten Paint stimmt.
Wie ein gestaltetes Dark Theme in der Praxis aussieht, aus unserer Token-Datei:
- Flächen werden heller, je weiter sie nach vorn kommen.
#000Seite,#121212Card,#1A1A1ASurface,#262626Popover. - Füllungen bleiben, weiche Flächen wechseln. Die Brand-Füllung ist in beiden Themes
#D63F00, weißer Text behält also 4.59:1. Ihre weiche Fläche wechselt von#FFEFE6zu#2A1509und ihr Text von#B03400zu#FF8A4C. - Die neutrale Füllung kehrt sich um.
primaryist im Light Mode schwarz mit weißem Text und im Dark Mode#FAFAFAmit#171717, also behält der neutrale volle Button 21:1 im Light und 17.16:1 im Dark. - Elevation wechselt die Technik. Ein Schlagschatten ist auf Schwarz kaum zu sehen, also setzen dunkle Schatten auf eine beleuchtete Oberkante und eine weiße Haarlinie (siehe die Elevation-Stufen unten).
- Der Fokusring verschiebt sich.
--ringistoklch(0.60 0.20 36.93)im Light undoklch(0.6617 0.2215 36.93)im Dark, auf Weiß also dunkler, damit eine 2px-Outline auf Weiß und auf der grauen Card 3:1 hält. - Manche Komponenten brauchen eine Ausnahme. Unsere weiche Card nutzt im Light
bg-card, im Dark aberbg-foreground/7, weil die dunkle--carddasselbe Grau hat wie eine dunkle Sidebar oder Stage und die Card dort verschwinden würde. Ein Skript, das Farben invertiert, trifft diese Entscheidung nie.
Wie prüfst du den Kontrast für jedes Token-Paar?
Liste die Paare auf, die zusammen genutzt werden sollen, rechne beide Farben nach sRGB um, berechne das WCAG-Verhältnis in beiden Themes und lass den Build unter 4.5:1 scheitern. WCAG verlangt 4.5:1 für Fließtext und 3:1 für großen Text (1.4.3 (opens in a new tab)) sowie 3:1 für UI-Komponenten und Fokusindikatoren gegenüber dem, was daneben liegt (1.4.11 (opens in a new tab)).
Unser Check deckt 54 Paare pro Theme ab, insgesamt 108, und meldet Stand Release 1.7.0 kein Paar unter 4.5:1. Die knappsten Paare sind brand-foreground auf brand mit 4.59:1 und muted-foreground auf der hellen card mit 4.62:1. Hier ist eine eigenständige Version, die du in scripts/ legen und mit npx tsx scripts/check-contrast.ts ausführen kannst. Sie liest Hex- und oklch()-Werte (Helligkeit als Dezimalzahl oder Prozent), folgt var()-Referenzen und beendet sich bei einem Fehlschlag mit Code 1. Sie erwartet die Token-Namen aus diesem Guide; wenn deine anders heißen, pass SURFACES, TONES und PAIRS an:
Diese gekürzte Version prüft 82 Paare. Unser vollständiges Skript ergänzt die übrigen Paare für Flächen, Primary und Legacy-Tokens und verrechnet transparente Werte vorher mit der Seite. Der wertvolle Teil ist die Paarliste: Sie hält fest, welcher Text auf welcher Fläche stehen darf, also wird ein neues Token an dem Tag geprüft, an dem es dazukommt. Lass es in CI neben dem Type-Check laufen.
Solltest du Tailwind v4 OKLCH-Werte oder Hex schreiben?
Nimm das Format, mit dem du am besten arbeiten kannst; das Format ändert den Kontrast nicht. Tailwinds eigene Palette ist in v4.0 von rgb auf oklch umgestiegen, um den größeren Farbraum zu nutzen (Release-Post (opens in a new tab)), und die Farbreferenz (opens in a new tab) listet jeden Tailwind v4 OKLCH-Wert, etwa --color-red-500: oklch(63.7% 0.237 25.331). OKLCH verdient seinen Platz, wenn du Farben im Code ableitest, denn die Helligkeit ist ein eigener Kanal: Du kannst sie verschieben, während Hue und Chroma bleiben, und das Ergebnis sieht immer noch nach derselben Farbe aus.
In unserer Datei sind Neutraltöne oklch() mit Chroma null (oklch(0.3211 0 0) ist der helle Foreground), und die Tone-Werte sind Hex, von Hand gegen den Kontrast-Check abgestimmt. Der Theme-Generator geht den umgekehrten Weg: Du wählst für jeden Tone eine Grundfarbe (Brand, Success, Warning, Danger, Info), und er leitet daraus Füllung, weiche Fläche und Text des Tones in OKLCH ab. Für den Light Mode setzt er die weiche Fläche auf Helligkeit 0.965 mit Chroma höchstens 0.035 und senkt dann die Helligkeit des Tone-Texts ab 0.6 in Schritten von 0.005, bis er 4.6:1 auf der weichen Fläche und auf jeder neutralen Fläche erreicht. Verschieb oklchToRgb und contrast aus dem Kontrast-Skript in ein eigenes Modul, scripts/color.ts, damit der Import den Check nicht startet, und dieselbe Suche passt in ein paar Zeilen:
Das Ziel ist 4.6 statt 4.5, damit das Runden auf Hex eine Farbe nicht unter die Grenze drücken kann. Eine Warnung noch: Ein OKLCH-Wert mit hoher Chroma kann außerhalb von sRGB liegen, und was ein Bildschirm dann zeigt, hängt von Display und Browser ab. Halte Token-Werte innerhalb von sRGB oder miss zumindest den begrenzten sRGB-Wert, so wie oklchToRgb es tut.
Wie werden Radius, Elevation, z-index und Motion zu Tokens?
Sie werden zu Skalen mit einer festen Zahl von Stufen, jede mit genau einer Aufgabe, sodass eine Komponente eine Stufe wählt statt einer Zahl. Ein Tailwind v4 Design System, das nur Farben tokenisiert, landet trotzdem bei Schatten-Einzelfällen und irgendwo bei einem z-[9999].
Der Radius folgt der Größe
Koppel den Radius an die Größe des Controls. Ein 32px- und ein 48px-Button wirken nur dann wie dieselbe Form, wenn der größere den größeren Radius hat. Unsere Controls übernehmen diese Map vom Button:
Flächen nutzen rounded-2xl (16px) für Cards, Menüs und Popovers, rounded-3xl (24px) für Dialoge und rounded-t-[28px] für Bottom Sheets. Alles innerhalb einer Fläche mit Padding bekommt den äußeren Radius minus Padding: Ein Menü mit rounded-2xl p-1.5 hat Items mit rounded-[10px]. Größen folgen derselben Logik: 32, 40 und 48px, wobei die 40px-Größe auf Touchscreens auf 44px wächst, die Regel aus unserem Guide zu Mobile-first React-Komponenten.
Die Falle ist ein globales Radius-Token. Unsere Datei trägt für portierte Komponenten noch die ältere Skala von shadcn/ui: --radius: 0.375rem, --radius-lg: var(--radius) und --radius-sm: calc(var(--radius) - 4px). Das biegt Tailwinds rounded-sm, rounded-md, rounded-lg und rounded-xl überall auf 2, 4, 6 und 10px um, auch in Markup, das du aus der Tailwind-Doku kopierst (das kompilierte .rounded-lg ist border-radius: var(--radius)), deshalb nutzen neue Komponenten diese vier Klassen nie. Stand Oktober 2026 leitet die Theming-Doku von shadcn/ui (opens in a new tab) die Skala stattdessen per Multiplikation ab (--radius-sm: calc(var(--radius) * 0.6) bis --radius-4xl), was dieselbe projektweite Wirkung hat. Wenn du den Radius global themst, musst du jede Komponente kontrollieren, die ihn nutzt.
Drei Elevation-Stufen
Drei Stufen reichen für eine Bibliothek: shadow-raised für Buttons, Cards und Thumbs, shadow-floating für Popovers, Menüs und Toasts, shadow-modal für Dialoge und Sheets. Jede Stufe bringt ihre eigene 1px-Haarlinie mit, eine erhöhte Fläche braucht also keinen Rahmen:
Eine interaktive Card wechselt beim Hover zwischen zwei dieser Stufen, und das Anheben läuft als CSS-Transition auf einer Spring-Kurve (siehe unten), also animiert ein Raster aus fünfzig Cards ohne JavaScript.
$ npx wingo-ui@latest add cardStapelebenen
Popovers liegen absichtlich über Modals: Ein Select in einem Dialog muss sich darüber öffnen. Kein Overlay in der Bibliothek nutzt eine rohe Zahl wie z-50; kleine Werte wie z-10 ordnen nur Teile innerhalb einer Komponente.
Motion
CSS-Transitions nutzen drei Dauern (duration-fast 150ms, duration-normal 240ms, duration-slow 400ms) und benannte Kurven (ease-smooth ist cubic-bezier(0.22, 1, 0.36, 1), ease-sheet ist cubic-bezier(0.32, 0.72, 0, 1)). Unsere JavaScript-Springs (die vier Framer Motion Spring-Presets) werden außerdem in CSS-linear()-Easings abgetastet: ease-spring mit duration-spring (500ms) entspricht einer Spring mit Stiffness 300 und Damping 30. Wenn das Betriebssystem reduzierte Bewegung anfordert, sollten diese Springs kurzen Fades weichen; unser Guide zu prefers-reduced-motion in React setzt das in Next.js ohne Hydration Mismatch um.
Können Komponenten-Props wie Design Tokens funktionieren?
Ja, für die Entscheidungen, die Props sind: der Radius jedes Buttons, die Standardgröße von Feldern, die Variante jeder Card. CSS-Variablen können kein Prop setzen, also liest Wingo UI diese Werte aus einem React-Provider, und UIProvider defaults wird zur Token-Schicht für Props. Der kostenlose UIProvider legt app-weite Defaults unter die Props, die jede Komponente bekommt, geschlüsselt nach ihrem data-slot-Namen:
In der Komponente liest die erste Zeile die Defaults, und die Defaults in der Signatur bleiben der letzte Fallback:
Explizite Props gewinnen, dann der Provider, dann die eigenen Defaults der Komponente. Ein undefined-Prop überschreibt keinen Provider-Wert, also können Wrapper Props durchreichen, ohne das Theme zu löschen. Dieselbe Signatur nimmt ref als normales Prop, ohne forwardRef-Wrapper; unser Migrationsguide zu forwardRef in React 19 bringt ältere Komponenten auf dieses Muster, TypeScript-Typen inklusive.
$ npx wingo-ui@latest add ui-configWie bringst du diese Tokens in ein bestehendes Next.js-Projekt?
Führ npx wingo-ui@latest init in einem Projekt mit Tailwind CSS v4 aus. Der Befehl schreibt wingo-ui.json und lib/utils.ts (den cn-Helper), installiert clsx und tailwind-merge und führt die Tokens in das Stylesheet zusammen, das @import "tailwindcss" enthält. Er ergänzt nur fehlende Namen und behält deine Werte; add bringt später alle Tokens mit, die eine neue Komponente braucht. Der komplette Block, 74 Variablen in :root plus ihre dunklen Werte und Tailwind-Mappings, liegt unter /r/tokens.css, und Coding-Agents bekommen dasselbe CSS über das MCP-Tool get_theme_tokens (siehe unseren MCP-Guide für Komponentenbibliotheken). Die Theming-Doku zeigt die wichtigsten Tokens mit ihren hellen und dunklen Werten und die zwei Dark-Mode-Setups, die die CLI unterstützt: eine .dark-Klasse oder prefers-color-scheme, wenn dein Stylesheet das schon nutzt.
Zum Umgestalten öffnest du den Theme-Generator, wählst eine Markenfarbe, Neutraltöne, Radius, Schrift und Komponenten-Defaults, prüfst seinen Kontrastbericht und wendest den Export an:
Der Befehl ersetzt die Werte der Tokens, die das Theme setzt, in Light und Dark, ergänzt Tailwind-Mappings für neue Tokens und gibt die UIProvider-Defaults und die Locale aus, die du in deinen Provider einfügst.
Wenn du von shadcn/ui kommst: Die gemeinsamen Namen passen zusammen, aber zwei haben eine andere Bedeutung. Unsere --card ist eine flache graue Kachel, während sie bei shadcn der Hintergrund der Card ist, und unser --accent ist ein oranger Tint. Weil init deine bestehenden Werte behält, behält ein shadcn-Projekt seine eigene --card und seinen eigenen --accent, prüf also nach dem Zusammenführen die weiche Card-Variante. Unser Vergleich der shadcn-Alternativen behandelt die größeren Unterschiede zwischen den beiden Bibliotheken.
Welche Fehler machen ein Tailwind v4 Design System kaputt?
Dieselben paar Fehler tauchen in den meisten Token-Dateien auf:
dark:-Klassen in Komponenten. Einer Komponente, diedark:bg-zinc-900braucht, fehlt ein Token.- Palettennamen in Komponenten.
bg-blue-600kann keinem Theme folgen; nimm eine Rolle oder einen Tone. - Füllfarben als Text.
text-destructiveauf einer dunklen Fläche fällt bei AA durch; nimm den Soft-Foreground. - Werte pro Theme in einfachem
@theme. Halte sie in:rootund.dark, gemappt mitinline. - Ein globaler Radius, der Tailwinds Skala umschreibt. Jedes eingefügte
rounded-lgändert sich mit. - Rohe z-index-Zahlen. Zwei
z-50-Overlays von zwei Autoren kollidieren. - Kein Kontrast-Check. Tokens driften mit jeder Anpassung; ein Check in CI fängt das noch am selben Tag ab.
Wo solltest du anfangen?
Führ npx wingo-ui@latest init in deinem Next.js-Projekt aus, dann npx wingo-ui@latest add button, das die Items Tones und UIProvider gleich mitbringt. Ersetz in einem echten Screen eine fest verdrahtete Farbe durch ein Rollen-Token, öffne ihn in beiden Themes und nimm das Kontrast-Skript in CI auf. Zwei weitere Guides in Next.js und Tailwind CSS behandeln den Komponentencode rund um die Tokens: die Migration von forwardRef in React 19 und einen SSR-sicheren React use mobile Hook, der ohne Mismatch hydriert. Für das Projekt drumherum folgst du dem Setup einer Next.js-Komponentenbibliothek für den App Router oder dem shadcn Vite Setup für eine Vite-App. Wenn du auf den Tokens eigene Komponenten schreibst, zeigt der Guide zum React Controllable State Hook, wie ein einziges value-Prop kontrolliert und unkontrolliert funktioniert. Für die Seiten, die sie ausliefern, baut Next.js JSON-LD typisierte strukturierte Daten, die zur Seite passen, und Next.js Cookie Consent ergänzt ein DSGVO-Banner mit Google Consent Mode v2.
Häufige Fragen
Wohin gehören Design Tokens in Tailwind v4?
In die globale CSS-Datei, nach @import "tailwindcss". Variablen in einem @theme-Block werden zu Utilities (--color-surface ergibt bg-surface), und Werte, die sich pro Theme ändern, stehen in :root und .dark und werden mit @theme inline gemappt. Eine tailwind.config.js brauchst du nicht mehr.
Was ist der Unterschied zwischen @theme und @theme inline in Tailwind v4?
Mit @theme verweist die Utility auf die Theme-Variable, bg-surface wird also zu var(--color-surface) kompiliert, und das wird einmal auf :root aufgelöst. Mit @theme inline bekommt die Utility den referenzierten Wert direkt, bg-surface wird zu var(--surface) und löst sich auf dem Element auf, das die Klasse nutzt. Nimm inline immer dann, wenn eine Theme-Variable auf eine andere Variable zeigt.
Sollten Farben in Tailwind v4 OKLCH oder Hex sein?
Beides funktioniert, denn Tokens sind ganz normale CSS-Farben, und das Format ändert den Kontrast nicht. Tailwinds eigene Palette ist mit v4.0 auf oklch umgestiegen. OKLCH hilft vor allem, wenn du Farben im Code ableitest, weil du Hue und Chroma behältst und nur die Helligkeit verschiebst, bis ein Kontrastziel erreicht ist.
Wie bekommen Tailwind v4 Design Tokens einen Dark Mode?
Definier dieselben Variablen in einem .dark-Block neu und ergänze @custom-variant dark (&:is(.dark *));, damit auch dark:-Utilities der Klasse folgen. next-themes mit attribute="class" setzt die Klasse vor dem ersten Paint. Komponenten mit semantischen Tokens brauchen dann keine dark:-Klassen.
Wie prüfe ich den Farbkontrast von Design Tokens?
Liste jedes Text-Token mit jeder Fläche auf, auf der es stehen kann, rechne die Werte nach sRGB um und berechne das WCAG-Verhältnis für Light und Dark. Lass den Build unter 4.5:1 für Fließtext scheitern; Fokusringe und andere UI-Formen brauchen 3:1.
Sind die Design Tokens von Wingo UI kostenlos?
Ja. npx wingo-ui@latest init führt die Tokens ohne Account in dein Stylesheet zusammen, und die Items Button, Card, Tones und UIProvider aus diesem Guide sind kostenlos. Stand Release 1.7.0 sind 75 der 326 Registry-Items kostenlos, der Rest braucht Pro.
- Tailwind CSS
- Design Tokens
- Dark Mode
- OKLCH
- Design-Systeme
- Barrierefreiheit


