Theme Overrides
Lumeo's theme system is a layered set of CSS custom properties. The library ships seven
built-in themes (Zinc, Blue, Green, Rose, Violet, Amber, Teal) in both light and dark mode.
You override the theme at three levels: globally with CSS variables, per-mode with dark-mode
swaps, and per-component with the Class
parameter. Every Lumeo component reads colors from variables — there are no hard-coded hex
values anywhere in the library — so a variable change cascades to every component on the page.
The variable system
Variables are declared on :root
for light mode and re-declared on .dark
for dark mode. They come in foreground / background pairs — every surface variable has a
matching -foreground for
readable text on top.
| Variable | Used by |
|---|---|
--color-background / --color-foreground |
Page surface and primary text. |
--color-card / --color-card-foreground |
Card, Sheet, Popover, Dropdown, Dialog body. |
--color-primary / --color-primary-foreground |
Primary button, active links, accent fills. |
--color-secondary / --color-secondary-foreground |
Secondary button, subtle chips. |
--color-muted / --color-muted-foreground |
Muted surfaces (skeletons, code blocks) and secondary text. |
--color-accent / --color-accent-foreground |
Hover backgrounds for menu items, navigation links. |
--color-destructive / --color-destructive-foreground |
Destructive buttons, invalid form rings, error alerts. |
--color-border |
Default border for inputs, cards, dividers. |
--color-input |
Form input borders. |
--color-ring |
Focus-visible ring color. |
--radius |
Base border-radius. Components derive --radius-sm / --radius-xl from it. |
Global override
To re-skin the entire app, override the variables in your own stylesheet loaded
after lumeo.css.
Use OKLCH for colors — the built-in themes do — so contrast stays predictable when you tweak
lightness.
/* my-app.css — loaded after lumeo.css */
:root {
--color-primary: oklch(0.55 0.22 250);
--color-primary-foreground: oklch(0.98 0 0);
--color-ring: oklch(0.55 0.22 250);
--radius: 0.5rem;
}Dark mode swaps
Lumeo does not use Tailwind's dark:
prefix anywhere internally. Dark mode flips the same variables under the
.dark class on
<html>. Your
overrides must declare both blocks or dark mode will fall back to the library defaults for
anything you didn't redeclare.
:root {
--color-background: oklch(1 0 0);
--color-foreground: oklch(0.15 0 0);
--color-primary: oklch(0.55 0.22 250);
--color-primary-foreground: oklch(0.98 0 0);
}
.dark {
--color-background: oklch(0.15 0 0);
--color-foreground: oklch(0.98 0 0);
--color-primary: oklch(0.65 0.18 250);
--color-primary-foreground: oklch(0.15 0 0);
}
The .dark class is
toggled by the ThemeService.
Don't toggle it yourself unless you also persist the choice.
Scoped override
Variables cascade, so you can re-declare them on any ancestor element to re-skin only the subtree below it. Useful for an embedded widget that should look "branded" while sitting inside a neutral app shell.
<div class="embedded-widget" style="--color-primary: oklch(0.6 0.2 30); --color-ring: oklch(0.6 0.2 30);">
<Card>
<Stack Gap="3" Class="p-4">
<Heading Level="3">Promo</Heading>
<Button>Get started</Button>
</Stack>
</Card>
</div>Per-component class overrides
Every Lumeo component accepts Class.
On the components migrated to the tailwind-merge resolver
(Cx.Merge) — the core set — a utility you pass
wins any conflict with a base utility (see Overriding component classes below).
Use it to add utilities (margins, hover states, sizing). For colors, prefer pointing at theme
variables — bg-primary,
text-foreground — so the
override still respects light and dark mode.
<!-- good: theme-aware utilities --> <Button Class="bg-success text-success-foreground hover:bg-success/90">Mark complete</Button> <!-- good: spacing / sizing --> <Card Class="max-w-md mx-auto shadow-xl">...</Card> <!-- avoid: raw hex breaks in dark mode --> <Button Class="bg-[#1e40af] text-white">Don't do this</Button>
Avoid raw hex / rgb in Class.
They look right in light mode and break in dark mode because they don't participate in the
variable swap.
Overriding component classes
Most Lumeo components compose their final class list through a tailwind-merge resolver
(Cx.Merge), not a naive
string concat. (A few primitives — e.g. Heading,
Text,
Link,
AppBar — are still being migrated and
append Class by concatenation; there a
conflicting utility may still need !important
until they move over.) When a utility you pass via
Class conflicts with one of
the component's base utilities, your class wins — and you no longer need
!important to make it stick.
So <Button Class="h-12"> or
<Badge Class="px-4">
just work; the old !h-12 /
!px-4 workarounds are obsolete.
(Override the utility on the element that actually carries it — e.g. a Card's padding lives on
CardContent/CardHeader,
so pass Class="p-0" there, not on the bare Card root.)
Conflicts are resolved per utility group — padding, margin, sizing, colors, border-radius, and so
on — so overriding h-12 replaces
only the height and leaves the component's other base utilities intact. Unknown or custom classes
(your own non-Tailwind class names) are never dropped — they're always kept in their original
source position. Arbitrary Tailwind values such as h-[42px]
or bg-[#fff] are not unknown —
they're classified into their utility group and follow the same last-wins rule.
<!-- Class wins the Tailwind conflict — no ! needed -->
<Button Class="h-12">Tall</Button> <!-- replaces the button's default height -->
<Badge Class="px-4">Wide</Badge> <!-- replaces the badge's default padding -->
<!-- override the element that actually carries the utility:
a Card's padding lives on its sub-components, not the Card root -->
<Card>
<CardContent Class="p-0">...</CardContent>
</Card>
<!-- per-group: only height is replaced, colors/radius stay -->
<Button Class="h-12">Tall, still primary, still rounded</Button>
<!-- custom / unknown classes are always kept -->
<Button Class="h-12 my-cta-button">...</Button>Picking the right layer
| Goal | Layer |
|---|---|
| Brand color across the whole app | Global override of --color-primary + dark variant. |
| Different look for an embedded marketing widget | Scoped override on the widget's wrapper. |
| One specific button needs a custom hue | Per-component Class with theme utilities. |
| Switch users between built-in themes | ThemeService + ThemeSwitcher. |
| Round all corners more | Global override of --radius. |
Demo: scoped theme
Wrap any subtree in a div that re-declares the variables. The card and button below pick up a violet accent without affecting anything else on this page.
Scoped accent
This card lives inside an override block.
See also
- Theme Service — toggling and persisting themes
- Theme Switcher — built-in switcher component
- Theme Toggle — light/dark toggle
- Accessibility — contrast guarantees