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.

Surfaces & text

Aa
--color-background· --color-foreground

Page surface and primary text.

Aa
--color-card· --color-card-foreground

Card, Sheet, Popover, Dropdown, Dialog body.

Aa
--color-muted· --color-muted-foreground

Muted surfaces (skeletons, code blocks) and secondary text.

Brand & accent

Aa
--color-primary· --color-primary-foreground

Primary button, active links, accent fills.

Aa
--color-secondary· --color-secondary-foreground

Secondary button, subtle chips.

Aa
--color-accent· --color-accent-foreground

Hover backgrounds for menu items and navigation links.

Feedback

Aa
--color-destructive· --color-destructive-foreground

Destructive buttons, invalid form rings, error alerts.

Lines & focus

--color-border

Default border for inputs, cards, dividers.

--color-input

Form input borders.

--color-ring

Focus-visible ring color.

Radius

--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.

Menu color

The customizer's "Menu color" setting forces the sidebar to a fixed light or dark appearance regardless of the page's own light/dark mode — useful for an app chrome that always keeps a dark rail. themeManager.setMenuColor("dark"|"light"|"default") writes a data-menu-color="dark"|"light" attribute on <html> — exactly like data-menu-accent — never an inline color. It replaces the whole --color-sidebar-* set with the ACTIVE theme's own light or dark sidebar colors, so a dark menu on the Green theme is Green's dark sidebar, not a hard-coded zinc.

@inject IJSRuntime JS

@* Force the sidebar dark regardless of light/dark mode *@
await JS.InvokeVoidAsync("themeManager.setMenuColor", "dark");

@* Back to following the active theme's own mode *@
await JS.InvokeVoidAsync("themeManager.setMenuColor", "default");

@* An embedded preview that must ignore the page-wide setting *@
<SidebarProvider IsolateMenuColor="true">
    ...
</SidebarProvider>

Theme authors: every built-in theme file (and lumeo.css's own default) declares 16 private variables in its light block — --_sidebar-light-{bg,fg,primary,primary-fg,accent,accent-fg,border,ring} and --_sidebar-dark-{…} — carrying that theme's light AND dark sidebar colors regardless of which mode is active. A custom theme must declare the same 16 (underscore-prefixed, private — not part of the public token contract) for Menu color to resolve correctly against it.

Embedded previews. A SidebarProvider with IsolateMenuColor="true" renders data-menu-color-isolate on its root and always follows the active theme's own light/dark sidebar instead of the page-wide Menu color setting — this docs site uses it on every component demo, pattern card, and the dashboard block so those previews render like an ordinary consumer app rather than reacting to whatever the customizer currently has selected.

See also

Override contracts: data-slot

Every component marks its root element with data-slot, named after the component in kebab-case: [data-slot=button], [data-slot=sidebar-menu-button], [data-slot=dropdown-menu-item]. Form controls whose root is the field wrapper also mark the control itself as <name>-control ([data-slot=input-control]). A stylesheet written against these selectors survives a release that changes a utility class; one written against .h-7 does not. Write overrides against the slot, never against the class list.

[data-slot=sidebar-menu-button] { font-size: 13px; }
[data-slot=input-control] { font-variant-numeric: tabular-nums; }

Geometry tokens

Lumeo is a NuGet package, so you cannot edit h-9 into h-8 in a component source the way a shadcn user does. Instead the geometry that products tune most reads from a small, fixed set of CSS variables. Each component consumes var(--lumeo-…, <default>) on its Comfortable rung, so nothing changes until you set a token; Density.Compact and Density.Spacious stay one step below and above as fixed presets. Set a token on :root for the whole app or on any ancestor for a region.

TokenDefaultConsumed by
--lumeo-control-h32pxButton (Default, Icon), Input, Select, DatePicker, TimePicker, Cascader, TreeSelect, ColorPicker, TagInput, NumberInput, PasswordInput
--lumeo-control-h-xs / -sm / -lg24 / 28 / 36pxButton Xs, Sm, Lg
--lumeo-icon-size16pxAn unsized icon inside Button, DropdownMenuItem, CommandItem, TabsTrigger, SidebarMenuButton
--lumeo-sidebar-item-h (-sm, -lg)32 (28, 48)pxSidebarMenuButton
--lumeo-table-head-h40pxTableHead
--lumeo-table-cell-p8pxTableCell padding, TableHead horizontal padding
--lumeo-grid-cell-px / -py12 / 8pxDataGrid header and body cells
--lumeo-grid-header-h32pxDataGrid header row `min-height` (Comfortable only — Compact keeps its own tighter, fixed height)
--lumeo-grid-row-h36pxDataGrid data row `min-height` (Comfortable only — Compact keeps its own tighter, fixed height)
--lumeo-menu-item-h32pxDropdownMenuItem/CheckboxItem/RadioItem/SubTrigger, ContextMenu and Menubar equivalents — `min-height` at Comfortable/default (DropdownMenu's Compact/Spacious rungs are fixed, not token-driven)
--card-spacing16pxCard inset and gap
--spacing4pxTailwind's spacing step; every token above is a multiple of it
--lumeo-calendar-cell-size / --lumeo-calendar-p32 / 12pxCalendar day cell, frame padding (DatePicker, DateRangePicker)
:root {
  --lumeo-control-h: 28px;          /* every button and field 28px tall */
  --lumeo-icon-size: 14px;
  --lumeo-sidebar-item-h: 30px;
  --lumeo-grid-cell-py: 6px;        /* a 36px grid row */
  --lumeo-grid-row-h: 2.5rem;       /* a 40px DataGrid data row (Comfortable) */
  --lumeo-menu-item-h: 2.25rem;     /* a 36px DropdownMenu/ContextMenu/Menubar item (Comfortable/default) */
}