Colors
How the token layer is built, and how to rebrand it.
This is Tailwind v4, configured CSS-first. There is no tailwind.config.ts and
no shared config package — everything lives in
packages/design-system/styles/globals.css, which each app imports:
@import "tailwindcss";
@import "@kreogen/design-system/styles/globals.css";There is no @kreogen/tailwind-config package, and no JavaScript object you
can import theme values from. In v4 the theme is CSS custom properties. Read
them with getComputedStyle if you genuinely need a value at runtime, but
prefer passing a var(--color-…) through to whatever needs it.
Three layers
The file is built in layers, and knowing which one you are editing is most of the battle.
1. @theme — the brand ramp. An eleven-step scale, --color-brand-50
through --color-brand-950. Because it is declared in @theme, Tailwind
generates real utilities from it: bg-brand-500, text-brand-700,
border-brand-200.
2. :root and .dark — semantic values. --background, --foreground,
--primary, --muted, --destructive and friends, defined twice: once for
light, once inside .dark. Branded ones point at a step in the ramp rather
than restating a colour.
3. @theme inline — the bridge. Maps each semantic variable to the
--color-* name Tailwind builds utilities from:
@theme inline {
--color-background: var(--background);
--color-primary: var(--primary);
}inline is what makes this work. Without it Tailwind resolves the variable
once, at build time, and the light value would be baked into the utility — so
.dark would change the custom property and nothing on screen.
A token missing from this layer produces no CSS at all, silently. That is
how text-destructive-foreground came to be a class that did nothing: the
value existed in :root, the mapping did not.
Usage
export const MyComponent = () => (
<div className="rounded-lg border bg-background text-foreground shadow">
<p>Using semantic tokens.</p>
</div>
);Prefer the semantic token over the ramp step. bg-primary follows a rebrand
and adapts to dark mode; bg-brand-700 does neither. Reach for the ramp when
you genuinely mean "this specific step" — a chart series, a gradient stop.
Rebranding
Edit the eleven lines of the @theme brand ramp. Every branded token points at
a step in it, so that is the whole surface — no component changes, no
find-and-replace.
Two things are worth knowing before you generate a ramp:
Colours are in oklch. Perceptual lightness is the first coordinate, so interpolating between steps produces an even-looking scale rather than one that bunches in the middle the way hex interpolation does.
Chroma has a ceiling. Push it too far at the light end and the colour falls outside sRGB; browsers clip it, and they clip it to a different hue, so step 200 comes out visibly off-brand. Taper chroma toward both ends rather than holding it constant.
Check contrast afterwards. Whichever step you use for --primary needs to work
both as a fill under white text and as link text on the background — those are
different ratios, and a step that passes one can fail the other.
Neutrals are deliberately left at zero chroma. A tinted grey fights the brand hue across large flat areas like tables and borders, where a shift of 0.005 is visible.
Everything inherits these tokens
Including the authentication screens. Sign-in, sign-up, the organization switcher and the members table are built from the same components as the rest of the application, so a rebrand reaches them with no extra configuration.
That is a deliberate consequence of self-hosting auth: a hosted widget can only be themed as far as its vendor's theming API allows, which never quite matches, and the seam is always visible.