Theming
Light, dark, and custom palettes.
Flow UI has no theming engine — every theme is just a CSS variable set on :root or a scoped parent. Toggling between them is one class swap.
Light and dark
Both ship out of the box. Add or remove .dark on <html> (or any parent) to switch the subtree.
function ThemeToggle() {
return (
<button
onClick={() => document.documentElement.classList.toggle("dark")}
>
Toggle theme
</button>
);
}For a system-respecting default, match prefers-color-scheme once at boot:
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("dark");
}Custom palette
Override tokens in your app's CSS after the Flow imports. Any component downstream picks them up automatically.
@import "@flow-industries/ui/styles/tokens.css";
@import "@flow-industries/ui/styles/base.css";
@import "@flow-industries/ui/styles/fonts.css";
:root {
--brand: oklch(0.7 0.2 40); /* orange brand */
--brand-foreground: oklch(0.15 0 0);
--radius: 0.25rem; /* sharper corners */
}
.dark {
--background: oklch(0.15 0 0);
--foreground: oklch(0.95 0 0);
--muted: oklch(0.22 0 0);
}See Tokens for every overridable variable.
Why OKLch
Colors are defined in OKLch instead of hex or HSL because:
- Perceptual uniformity — equal numeric steps feel like equal brightness steps.
- Meaningful interpolation —
color-mix()between two OKLch colors doesn't pass through muddy grays. - Wide-gamut ready — maps cleanly to P3 and Rec2020 on modern displays.
Converting from hex: use oklch.com or any culori-based tool.
Scoped themes
Because every token is a plain CSS variable, you can scope a palette to any subtree:
<div className="brand-purple">
<Button variant="brand">Purple brand</Button>
</div>.brand-purple {
--brand: oklch(0.65 0.2 290);
}Only the button inside the wrapper sees the override. Outside, --brand stays pink.
No focus rings
Flow UI doesn't use ring utilities anywhere. Focus indication swaps border-color to --focus and (optionally) bumps --border-width-focus. The result: stable layout across focus transitions, no halo outside the component's bounding box.
If you override --focus, every focused element in the UI follows.