Theming

Per-product color customization via OKLCH palette generation. Pass two hex values — get a full 11-shade palette and a CSS override ready to inject.

@hilum/ui/create-themeNo external dependencies · Works in Node + browser

How To Use This Page

Theming explains how brand tokens and generated palettes flow through the component system.

Start here when you need to adapt the catalog to a product identity without forking the component code.

How The Content Is Grouped

  • The top-level Theming page groups the catalog by subcategory, making it easier to choose a family of patterns before drilling into an implementation.
  • Use the highlighted links below to start with the highest-signal areas of the section.

Installation

pnpm add @hilum/ui

Then import the base token CSS in your globals:

/* globals.css */
@import "tailwindcss";
@import "@hilum/ui/tokens.css";

Peer dependencies

react@^19 and react-dom@^19 are required.tailwindcss@^4 is required in any app that uses the token utilities.

Palette generator

Pick any two hex colors. The algorithm treats your input as the 500-level anchor and generates 11 perceptually-uniform shades via OKLCH color space interpolation.

Interactive preview

Change the colors to see the generated palette and how components adapt

Primary

Primary-500 anchor · 11 shades generated

Secondary

Secondary-500 anchor · 11 shades generated

Primary

Secondary

Live component preview

BrandSuccessWarning

Usage

ThemeProvider (recommended)

Wrap your app root. Re-applies automatically when props change.

import { ThemeProvider } from "@hilum/ui/create-theme"

export function App() {
  return (
    <ThemeProvider primary="#0066FF" secondary="#FF9900">
      <Router />
    </ThemeProvider>
  )
}

applyTheme()

Imperative alternative. Useful for Electron apps that apply a theme at init before React mounts.

import { applyTheme } from "@hilum/ui/create-theme"

// Call once at app startup (browser / Electron only)
applyTheme({ primary: "#0066FF", secondary: "#FF9900" })

createTheme() — pure / build-time

Returns the raw CSS string and palette. Works in Node — use it in build scripts, SSG pipelines, or to write a static CSS file per product.

import { createTheme } from "@hilum/ui/create-theme"

// Pure function — works in Node too (build scripts, SSG)
const { css, palette } = createTheme({
  primary:   "#0066FF",   // 500-level anchor → 11 shades generated
  secondary: "#FF9900",
})

// css  → inject into <style> or write to a .css file
// palette.primary["50"]  → "#f2f7ff"  (lightest tint)
// palette.primary["500"] → "#0066ff"  (your exact input)
// palette.primary["950"] → "#000723"  (darkest shade)

Manual CSS override

For non-React consumers or when full palette generation isn't needed. Import after tokens.css.

/* product-theme.css — loaded after tokens.css */
:root {
  --color-brand-primary:   #0066FF;
  --color-brand-secondary: #FF9900;
  --primary:               #0066FF;
  --primary-foreground:    #ffffff;
  --accent:                #f2f7ff;
  --accent-foreground:     #003d9f;
  --ring:                  #0066FF;
}

What gets overridden

The generated CSS overrides CSS custom properties that back Tailwind v4 utilities. Unlayered :root rules beat @layer theme, so the override takes effect without rebuilding Tailwind.

CSS variable

Value

Affects

--color-brand-primary

primary hex

bg-brand-primary, text-brand-primary, ring-brand-primary — Button, Badge, Input focus rings

--color-brand-secondary

secondary hex

bg-brand-secondary, border-brand-secondary — Badge success/warning, Button brand

--primary

primary hex

Semantic primary token

--primary-foreground

auto (WCAG)

Text on primary surfaces

--accent / --accent-foreground

primary-50 / primary-700

Hover tints, accent surfaces

--ring

primary hex

Focus rings on all interactive elements

--warning / --warning-foreground

secondary-200 / ground-900

Warning banners and callouts

--color-primary-{50…950}

generated palette

Product CSS: var(--color-primary-100), etc.

--color-secondary-{50…950}

generated palette

Product CSS: var(--color-secondary-100), etc.

API reference

Exports

Export

Signature

Description

createTheme(config)ThemeConfig → ThemeResult

Pure function. Generates palettes and returns { css, palette }. Works in Node and browser.

applyTheme(config)ThemeConfig → () => void

Injects a <style> tag into document.head. Returns a cleanup function. Browser / Electron only.

<ThemeProvider>ThemeConfig + children

React component. Calls applyTheme in useEffect, auto-cleans up on unmount or prop change.

ThemeConfig

Prop

Type

Description

primarystring

Hex color. Used as the 500-level anchor for the primary palette.

secondarystring

Hex color. Used as the 500-level anchor for the secondary palette.

ThemeResult

Field

Type

Description

cssstring

Complete CSS override string. Inject via <style> or write to a .css file.

palette.primaryRecord<string, string>

Generated shades: { "50": "#f2f7ff", …, "950": "#000723" }

palette.secondaryRecord<string, string>

Same structure for the secondary color.

How palettes are generated

Palette generation uses the OKLCH color space — a perceptually uniform model where equal numerical steps produce equal perceived changes. No external dependency; the conversion math is inlined (~100 lines).

  1. 1.

    Your hex input is parsed and converted to OKLCH: (L, C, H)

  2. 2.

    The input is anchored at shade-500. Shades 50–400 interpolate L toward 0.975 (near-white) with chroma fading 80%. Shades 600–950 interpolate L toward 0.145 (near-black) with chroma fading 40%.

  3. 3.

    Each generated OKLCH color is clamped to the sRGB gamut via binary search on chroma — ensuring every shade is a valid, displayable color.

  4. 4.

    Foreground colors (primaryForeground) are chosen using WCAG relative luminance with a 0.179 threshold — the crossover point where white and black have equal contrast.