Tailwind v4 Theme Generator
Brand colours to a complete @theme block, with dark mode.
Tokens
bg-ground · The furthest-back surface.
bg-surface · Panels and cards.
text-ink · Primary reading colour.
text-ink-soft · Secondary copy.
border-line · Hairlines and dividers.
bg-brand · Primary buttons and links.
text-accent · One restrained highlight.
Preview
Card heading
Muted supporting copy sits underneath.
Primary actionCard heading
Muted supporting copy sits underneath.
Primary actionAccessibility
3 issues- Accent on page in light mode is 2.97:1, below the 3:1 minimum.
- Borders on page in light mode is 1.24:1, below the 3:1 minimum.
- Borders on page in dark mode is 1.28:1, below the 3:1 minimum.
globals.css
@import "tailwindcss";
/* Tokens are declared once and exposed to Tailwind through @theme inline, so
every utility resolves to the variable rather than a baked-in value. */
@theme inline {
--color-ground: var(--ground);
--color-surface: var(--surface);
--color-ink: var(--ink);
--color-ink-soft: var(--ink-soft);
--color-line: var(--line);
--color-brand: var(--brand);
--color-accent: var(--accent);
}
:root {
--ground: #faf7f3;
--surface: #ffffff;
--ink: #1a1416;
--ink-soft: #6e5c62;
--line: #e8ded4;
--brand: #6b0f1a;
--accent: #b8890f;
color-scheme: light;
}
/* Explicit opt-in, for a theme toggle that writes data-theme. */
:root[data-theme="dark"] {
--ground: #221a10;
--surface: #121212;
--ink: #d6cbce;
--ink-soft: #99878d;
--line: #392d21;
--brand: #e67c89;
--accent: #e5b73e;
color-scheme: dark;
}
/* System preference, unless the visitor has explicitly chosen light. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--ground: #221a10;
--surface: #121212;
--ink: #d6cbce;
--ink-soft: #99878d;
--line: #392d21;
--brand: #e67c89;
--accent: #e5b73e;
color-scheme: dark;
}
}
@layer base {
body {
background: var(--ground);
color: var(--ink);
}
}Pick your brand colours and get a complete Tailwind v4 globals.css: an @theme inline block, CSS variables for light mode, a derived dark mode, and the media query that respects a visitor's system preference without overriding an explicit choice. Every pair is contrast-checked as you go.
Why does this not output a tailwind.config.js?
Because Tailwind v4 does not use one for theming. The JS config was replaced by a CSS-first setup where tokens are declared in an @theme block and utilities resolve from CSS variables. Generators still emitting a v3 config produce something that simply will not apply in a v4 project.
What does @theme inline do?
It registers your CSS variables with Tailwind so they become real utilities — declare --color-brand and you get bg-brand, text-brand and border-brand. The inline keyword makes Tailwind reference the variable rather than copying its value, which is what lets one set of utilities switch between light and dark at runtime.
How is the dark mode derived?
By reflecting each colour's lightness around the midpoint and compressing the result slightly, so near-white surfaces become near-black rather than pure black, and mid-tone brand colours stay recognisable. It is a sound starting point, not a substitute for a designer's eye — expect to hand-tune a couple of values.
Why both a data-theme selector and a media query?
The media query respects the operating system preference by default. The data-theme selector lets a toggle override it. The generated CSS scopes the media query so an explicit light choice is not stomped by a dark system setting — the bug almost every hand-rolled theme toggle ships with at first.
Can I add more tokens?
Yes. The output is ordinary CSS, so extra variables follow the same pattern: declare one in :root, redeclare it in both dark blocks, and expose it through @theme inline if you want a utility for it.
We build the kind of software this tool is a small piece of — web platforms, mobile apps and design systems, end to end.