Design System

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:

apps/app/app/styles.css
@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

component.tsx
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.