Skip to content

Foundations

Themes & typography

A theme is an attribute, not a stylesheet. One palette changes the brand accent across every component; one typeface attribute re-points the sans, display and mono roles. Both are scoped, so they apply to the document — or to a single card.

Seven palettes, one design system

Every palette below is a live subtree: the same Public API, the same tokens, one data-palette attribute. Nothing is re-implemented for the preview and nothing here needs JavaScript to render.

LUC Cyan

The default. Cool, technical, quiet.

cyan
Live
Electric Violet

AI products, creative tools, consumer.

violet
Live
Cobalt

Enterprise software and infrastructure.

cobalt
Live
Emerald

Health, growth, sustainability.

emerald
Live
Rose

Commerce, community, publishing.

rose
Live
Ember

Warm editorial, high energy.

ember
Live
Graphite

Monochrome — the content is the colour.

graphite
Live

Why a sparkline is in the card

Chart colour is part of the theme, not a separate configuration. The accent series, the selection band and the pinned marker all resolve from the palette, so a chart drawn inside this card cannot disagree with the button above it.

What a palette changes — and what it must never change

The line between brand and meaning is the whole reason the palette layer is small.

Changes with the palette

  • accent.default · hover · active · strong
  • accent.soft — the tint behind a selected or secondary surface
  • fg.accent — accent-coloured text
  • focus.ring — the focus language follows the brand
  • surface.selected · line.accent · glow.*
  • chart.marker · chart.selection · chart.band

Never changes with the palette

  • positive · negative · warning · info — meaning, not brand
  • the surface stack (canvas → sunken → default → raised → overlay)
  • the four text levels
  • radius, spacing, density and motion
  • component behaviour, semantics and keyboard handling

A palette that repainted “negative” green would break every product built on Luc UI. That is why the status set has no palette override at all — the token file simply does not offer one.

Contrast is measured, not assumed

Two pairs can break when an accent moves: the label on a filled accent control, and accent text on the default surface. Both are computed from the declared oklch values in unit tests, then re-measured in the browser by axe.

PaletteLabel on accentAccent text on surface

LUC Cyan

5.49 / 9.666.37 / 11.30

Electric Violet

6.46 / 8.927.46 / 10.46

Cobalt

6.19 / 9.027.16 / 10.59

Emerald

5.53 / 9.726.38 / 11.36

Rose

6.55 / 8.797.56 / 10.37

Ember

6.32 / 8.977.31 / 10.49

Graphite

16.26 / 14.7714.59 / 12.93

The three columns are the same numbers the unit test asserts. Values are WCAG 2.1 contrast ratios computed from the declared colour; the darker figure is the light theme, the lighter figure the dark theme. Every pair clears 4.5:1 in both themes — the floor for body-size text.

Type roles, not fonts

Components never name a family. They use one of three roles — sans, display, mono — and a preset re-points all three at once. A consumer with licensed brand fonts points the variables at their own stack and never edits a component.

Inter

inter

Neutral, complete, self-hosted.

Measured drawdown, not modelled drawdown

The role system means an editorial product can set body copy in a serif with looser leading while the data column stays monospaced and tabular, without a single component knowing which face is which.

−18.4%

Humanist system

humanist

No webfont: the platform's own sans.

Measured drawdown, not modelled drawdown

The role system means an editorial product can set body copy in a serif with looser leading while the data column stays monospaced and tabular, without a single component knowing which face is which.

−18.4%

Editorial serif

editorial

Serif voice with looser leading.

Measured drawdown, not modelled drawdown

The role system means an editorial product can set body copy in a serif with looser leading while the data column stays monospaced and tabular, without a single component knowing which face is which.

−18.4%

Technical mono

technical

Monospaced display for engineering surfaces.

Measured drawdown, not modelled drawdown

The role system means an editorial product can set body copy in a serif with looser leading while the data column stays monospaced and tabular, without a single component knowing which face is which.

−18.4%

Developer contract

Everything above is downstream of three attributes and one token layer.

<html data-theme="dark" data-palette="violet" data-typeface="editorial">/* A subtree theme is an attribute on any element. */ <section data-palette="graphite" data-theme="light"> <PricingTable /> </section>
  • data-palette — one of the seven accent families. Status colours are deliberately not part of it.
  • data-typeface — a preset that re-points the type roles. Point --font-sans, --font-display and --font-mono at your own stack to add one.
  • data-density and data-motion sit alongside them and compose freely: they are the same mechanism, not a second theming API.

Adding a palette

A new palette is ten declarations per mode. Copy an existing block in packages/ui/src/tokens/tokens.css, keep the lightness trajectory, change the hue — and the completeness and contrast tests will tell you if the ramp is incomplete or a label colour fails.

API

Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.

`ThemeProvider` is the mechanism under everything on this page: it writes the three attributes, resolves `system` against the operating system, and exposes `useTheme` to the product.

Exported symbols
ExportKindSignature
ThemeProvider4 documented props({ children, defaultTheme, storageKey, attribute, }: ThemeProviderProps) => import("@types/react").JSX.Element
useOptionalThemeexport() => ThemeContextValue | null
useThemeexport() => ThemeContextValue

ThemeProvider props

Type: ThemeProviderProps

Values the compiler reads from the declaration.

ThemeProvider props
PropTypeDefaultNotes
attribute?"class" | "data-theme" | undefined—Attribute written on `<html>`. `class` drives the `dark:` variant.
childrenReactNode—
defaultTheme?ThemeMode | undefined—
storageKey?string | undefined—
Luc UI v1.0Themes and typedark theme