webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
Tailwind CSS · Lesson 3 of 6

Themes and design tokens

@theme, semantic tokens, light/dark with CSS variables and a manual toggle.

Updated

What a theme is

A theme is a product's set of visual decisions: colors, fonts, radii, shadows, spacing. In a well-built project, these decisions are centralized in design tokens — named variables — and components use only the tokens.

The result: you change the brand, add dark mode or a second brand from a single file, without touching the components.

Tokens, not hardcoded colors

The classic mistake: bg-blue-600 scattered across 200 files. Change the brand → change 200 files.

The right way: you define semantic design tokens (what role the color plays, not what shade it is) and use them everywhere.

primitive:  blue-600, stone-100          → the raw palette
semantic:   accent, bg, surface, muted   → what the UI uses

@theme — tokens that become classes

@import "tailwindcss";

@theme {
  --color-accent: #1f5fbf;
  --font-display: "IBM Plex Sans", sans-serif;
  --radius-card: 0.75rem;
}

Every variable in the right namespace generates classes: --color-accent → bg-accent, text-accent, border-accent/50; --font-display → font-display; --radius-card → rounded-card.

Namespace Classes
--color-* bg-*, text-*, border-*, fill-*…
--font-* font-*
--text-* text-* (sizes)
--spacing the base for p-*, m-*, gap-*
--radius-* rounded-*
--shadow-* shadow-*
--breakpoint-* sm:, md:…

Light / dark with CSS variables

The pattern used in this very app:

:root {
  --bg: #f7f6f3;
  --fg: #1f1e1c;
  --accent: #1f5fbf;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #141414;
    --fg: #e9e7e2;
    --accent: #6ea4f0;
  }
}

@theme inline {
  --color-bg: var(--bg);
  --color-fg: var(--fg);
  --color-accent: var(--accent);
}

@theme inline = the class uses var(--bg) directly, so it changes when the variable changes. Components just write bg-bg text-fg — they don't need dark: at all.

A manual toggle (a theme button)

By default, dark: follows the system. For a button that forces the theme:

@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

[data-theme=dark] {
  --bg: #141414;
  --fg: #e9e7e2;
}

You put data-theme="dark" on <html> (saved in a cookie/localStorage, applied before hydration so it doesn't flicker).

Light / dark / system, with no "flash"

A real toggle has three options: light, dark and "like the system". The classic problem: the page renders on the server without knowing the user's choice → for a split second it shows in the wrong theme (a flash), then the JS corrects it.

Where you save the choice How you avoid the flash
a cookie the server reads the cookie and sets data-theme on <html> directly — zero flash (the page becomes dynamic)
localStorage a small script, inline in <head>, sets data-theme before the page is painted — what the next-themes library does
/* "system" = no attribute → follows prefers-color-scheme */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

@media (prefers-color-scheme: dark) {
  :root:not([data-theme=light]) { --bg: #141414; --fg: #e9e7e2; }
}
[data-theme=dark] { --bg: #141414; --fg: #e9e7e2; }

Also add color-scheme: light dark to :root — scrollbars and native controls (inputs, selects) adapt automatically.

Several themes / brands

The same mechanism: [data-theme=ocean] { --accent: ... }. The components don't change at all.

Rules

  1. Components use only semantic tokens (bg-surface, text-muted), not bg-stone-100.
  2. Few, well-named tokens: bg, surface, fg, muted, line, accent, success, danger.
  3. Check the contrast in both themes.

Summary

  • Semantic tokens (bg, surface, accent), not raw colors in components.
  • @theme creates classes; @theme inline + CSS variables = themes that change at runtime.
  • Dark mode and multiple brands = different values for the same variables.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.