Lumeo

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