FLOWDocs

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.

On this page