Theming with tweakcn / shadcn
tweakcn
is a visual theme editor for the shadcn/ui ecosystem. Lumeo's color tokens are
natively compatible with it: paste a shadcn / tweakcn
:root /
.dark export and the whole library
re-skins — no adapter, no import, no renaming.
TL;DR
- Build a theme at tweakcn, export the Tailwind v4 / CSS-variables form (OKLCH).
- Paste its
:root/.darkblocks into your app, unlayered and loaded afterlumeo.css. - Colors,
--radius,--shadow-*,--spacingand--font-sansdrive Lumeo 1:1 — Tailwind v4 reads them as variables. tweakcn's--tracking-*does not map (Lumeo reads different names — see caveats), and Lumeo's own status colors have no tweakcn source. Both keep Lumeo's defaults.
Verified against the prebuilt lumeo.css + lumeo-utilities.css in a headless browser (light + dark, computed styles).
How it works
shadcn / tweakcn write unprefixed variables
(--primary); Lumeo's components read the
Tailwind v4 --color-*
namespace. Lumeo maps the two natively at the source — every color token is defined as the shadcn variable with
Lumeo's own value as the fallback (no separate adapter or import file involved):
/* lumeo.css — every color token maps to the shadcn name (built in, no import) */
:root {
--color-primary: var(--primary, oklch(0.21 0.006 286));
--color-background: var(--background, oklch(1 0 0));
--color-border: var(--border, oklch(0.92 0.004 286));
--color-ring: var(--ring, oklch(0.21 0.006 286));
/* …every color token, base + dark + all palettes… */
--radius: 0.75rem; /* same name as shadcn — nothing to map */
}
So if you set --primary, it flows straight
into --color-primary and every
bg-primary / button / ring picks it up. Colors work
regardless of load order because this is variable indirection, not a cascade override.
The non-color tokens are different. Lumeo declares --radius
and the --shadow-* scale itself (unlayered), and
--spacing /
--font-* come from Tailwind's
@layer theme defaults. A tweakcn export is an
unlayered :root, so
pasted after the Lumeo import it wins the cascade over both — which is exactly why the order matters for these.
Steps
1 — Generate the theme
At tweakcn,
dial in your colors and corner radii, then Code / Export. Choose
OKLCH and Tailwind v4, and copy the
:root and .dark blocks.
2 — Confirm the Lumeo import, then paste after it
Your global stylesheet must import Lumeo's CSS. Paste both tweakcn blocks into the same file after that import, unlayered.
Tailwind v4
@import "./_content/Lumeo/css/lumeo.css" layer(base);
/* paste your tweakcn / shadcn export below — unlayered, so it wins */
:root { --primary: oklch(0.205 0 0); --radius: 0.625rem; /* … */ }
.dark { --primary: oklch(0.985 0 0); /* … */ }
Without Tailwind (prebuilt CSS assets)
<link href="_content/Lumeo/css/lumeo.css" rel="stylesheet" />
<!-- tweakcn export AFTER lumeo.css so it wins the cascade -->
<style>
:root { --primary: oklch(0.205 0 0); --radius: 0.625rem; /* tweakcn light vars */ }
.dark { --primary: oklch(0.985 0 0); /* tweakcn dark vars */ }
</style>
Load order matters for everything except colors. You can ignore the
@import "tailwindcss", @theme inline
and @layer base lines in the tweakcn export — Lumeo already provides those.
3 — Dark mode is wired automatically
Lumeo's ThemeService toggles the .dark class on
<html> — exactly the selector tweakcn's .dark block targets. Light and dark switch with no extra wiring.
4 — Rebuild CSS
If you run a Tailwind build step (the Lumeo PostCSS preset or tailwindcss --watch),
let it rebuild so the new custom-property values are picked up. Using the prebuilt CSS assets, a browser refresh is enough.
What carries over — and what doesn't
| Token group | Carries over | Notes |
|---|---|---|
| All colors | Yes |
background, foreground, card, popover, primary, secondary, muted, accent, destructive (+ foreground), border, input, ring, chart-1…5, and the full sidebar family — all map 1:1, order-independent (variable indirection). |
--radius |
Yes |
Same name in both. Lumeo derives --radius-sm/md/lg/xl from it, so set only the master --radius (don't paste tweakcn's derived scale). Must be pasted after Lumeo to win. (Verified: 0.75rem → 1.4rem moved rounded-lg from 15px → 28px.) |
--spacing |
Yes |
Tailwind v4 compiles Lumeo's spacing utilities to calc(var(--spacing) * n), so an unlayered --spacing pasted after Lumeo rescales every padding / margin / gap / size. (Verified: 0.25rem → 0.5rem doubled p-4 16px → 32px and gap-2 8px → 16px.) |
--shadow-* |
Yes |
Lumeo maps the shadow-* utilities to var(--shadow-*) (overriding only --tw-shadow, so focus-ring composition stays intact). A pasted --shadow-2xs … --shadow-2xl drives every elevation 1:1, again pasted after Lumeo. (Verified via a custom --shadow-md.) |
| Fonts | Partial |
--font-sans re-types the whole UI (Lumeo's body inherits it). --font-mono / --font-serif flow too, but only reach elements that use the font-mono / font-serif utilities — Lumeo components default to sans. (Verified: --font-sans and --font-mono both applied to their targets.) |
Tracking (--tracking-*) |
No |
tweakcn exports --tracking-normal (plus a @theme inline scale), but Lumeo's utilities read --tracking-tight/tighter/wide/… by name — names the copied :root / .dark blocks never set, so tracking is untouched. Set those exact names yourself to drive it. (Verified: --tracking-normal was inert; --tracking-tight set directly worked.) |
| Lumeo status colors | No |
tweakcn never emits info / success / warning / positive / rating (or their -light / -text variants), so they keep Lumeo's defaults. Override the --color-* directly if a recolored palette clashes. |
Caveats
- Paste order matters for everything but colors.
--radius,--shadow-*,--spacingand the--font-*tokens are declared by Lumeo or by Tailwind's@layer theme, so the tweakcn block must come after the Lumeo import and stay unlayered to win the cascade. Colors are exempt (variable indirection). - Tracking does not carry over.
A standard tweakcn copy leaves
--tracking-*inert because of the name mismatch above. Set--tracking-tightetc. directly if you need it. - Fonts are partial.
--font-sansre-types the UI;--font-mono/--font-serifonly affect elements that opt into those utilities. - Prebuilt assets vs your own Tailwind build behave the same.
Whether you load Lumeo's prebuilt
lumeo-utilities.cssor compile your own Tailwind v4 build over Lumeo's markup, the utilities resolve the samevar(--…)tokens, so the same variables flow and the same tracking gap applies. An own build gives you one extra lever: you can map--tracking-*(or anything else) in your own@theme.
Demo: a shadcn palette driving Lumeo
This block sets the bare shadcn names (--primary / --ring)
on one subtree — exactly what a pasted tweakcn export does globally — and Lumeo picks up the teal accent with no
extra wiring.
shadcn accent
Driven by a bare --primary, natively.
See also
- Theme Overrides — the underlying CSS-variable system
- Theme Service — programmatic light/dark switching
- Theme Switcher — switching between built-in palettes
- tweakcn editor — build and export a palette