Skip to content
ColorScreens

Theme builder

Every color tool ends at hexes, and nobody's job ends at hexes. This one ends at a stylesheet: give it a palette and it returns fourteen named tokens in light and dark, with every text, border and button pair measured against its WCAG floor and corrected rather than flagged. The preview below wears the result, and the export is the same object.

Your palette

The most saturated color leads; click any color to lead with it instead.

The theme, worn by a page

AcmeOverviewReportsSettings

Quarterly report

Body text on a surface, held to its ratio. An inline link.

Secondary text, quieter and still passing.

Saved successfully

Storage almost full

Payment failed

Every token, with its measured ratioLight mode. Click a value to copy it.

Each token of the generated theme with its hex value and, where the token carries a contrast requirement, the measured ratio against its ground
SwatchTokenValueChecked
backgroundThe page itself, behind everything.--
surfaceCards and panels. Sits on the background.--
borderCard and divider edges. Quiet on purpose: a decorative line at full contrast reads as a wireframe.1.50 vs surface
border-strongInput and control edges. Held to 3:1 against the surface, the non-text floor.3.02 vs surface
textBody text. Held to 7:1 against the surface where the palette allows, 4.5:1 always.14.54 vs surface
text-mutedSecondary text. Held to 4.5:1 against the surface.7.37 vs surface
linkInline links. Text carries a higher floor than a fill, so links get their own step of the primary at 4.5:1.4.50 vs surface
primaryButton fills. Held to 3:1 against the surface, with an ink that passes on it.4.11 vs surface4.53 vs on-primary
primary-hoverThe hover state: a step from the primary you can actually see, with the same ink still passing.5.83 vs on-primary
on-primaryText on the primary. Held to 4.5:1 against it.4.53 vs primary
focusFocus rings. Held to 3:1 against the background.3.87 vs background
successConfirmation. The triad is spread in lightness, so red-green blindness can still tell it apart.4.53 vs surface
warningCaution. The amber, carried lighter than its siblings on purpose.3.01 vs surface
dangerErrors. The red, carried darker than its siblings on purpose.8.66 vs surface
:root {
  --background: #FEF4F7;
  --surface: #FFFDFD;
  --border: #D9CFD2;
  --border-strong: #9B9194;
  --text: #2E2629;
  --text-muted: #5B5355;
  --link: #E4007E;
  --primary: #EE0E85;
  --primary-hover: #FF4297;
  --on-primary: #111111;
  --focus: #EE0E85;
  --success: #00865F;
  --warning: #B78E00;
  --danger: #921B1A;
}

[data-theme="dark"] {
  --background: #1D1316;
  --surface: #291E21;
  --border: #463B3E;
  --border-strong: #74686B;
  --text: #EFE0E4;
  --text-muted: #B8AAAE;
  --link: #F75097;
  --primary: #F75097;
  --primary-hover: #FF75AA;
  --on-primary: #111111;
  --focus: #F75097;
  --success: #00C28A;
  --warning: #F4C43A;
  --danger: #CC544B;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --background: #1D1316;
    --surface: #291E21;
    --border: #463B3E;
    --border-strong: #74686B;
    --text: #EFE0E4;
    --text-muted: #B8AAAE;
    --link: #F75097;
    --primary: #F75097;
    --primary-hover: #FF75AA;
    --on-primary: #111111;
    --focus: #F75097;
    --success: #00C28A;
    --warning: #F4C43A;
    --danger: #CC544B;
  }
}

What the dark remap actually does

Worked on one color, the site's own #7A5CFF, so every value here is the builder's real output rather than an illustration.

  • The grounds rebuild, not flip

    The light background #F6F6FF does not become its inverse. The dark background is #15151D, a value just above black that keeps the same faint tint of the brand hue, and the surface #21212A sits lighter than it, because in a dark interface light attaches to elevation: a card closer to you catches more of it.

  • Text steps back from the edge

    Inverting would put white on black at 21:1, which reads as glare. The dark text #E3E3F0 against #21212A lands in the comfortable band while the light pair holds 14.4:1, and both clear the AAA floor of 7:1 by construction.

  • The primary moves up and calms down

    #7A5CFF becomes #7859FC in light mode and #9183FF in dark: lighter, and at 85 percent of the chroma, which is the move every design system's dark variant makes. A saturation tuned for white paper vibrates against near-black.

  • Every floor is re-solved against the new grounds

    A pair that passed in light passes in dark because it was solved again there, not because it was carried over. The ratio beside each token in the table above is measured on the emitted value.

Common questions

What is a theme, as opposed to a palette?
A palette is colors; a theme is jobs. A theme names which value is the page, which is a card, which carries body text, which a button can trust its label on, and what every one of those becomes in dark mode. Going from one to the other is exactly the work this tool does: it turns five swatches into fourteen named tokens, twice.
Why is dark mode not just the light theme inverted?
Three reasons, all visible the moment you try it. Inversion puts pure white text on pure black at 21:1, which is glare rather than contrast; comfortable dark themes sit nearer 12 to 15:1. It keeps the primary at a lightness tuned for a white page, where it now vibrates against the dark one. And it flips elevation: in a dark interface a raised card must be lighter than the page behind it, and inversion makes it darker. The builder rebuilds each role against dark-mode targets instead.
What does "contrast-verified" mean here?
Every token with a legibility job carries a floor from WCAG 2.1: 4.5:1 for text, links and button labels, 3:1 for control borders, focus rings and button fills under the non-text rule. The decorative border deliberately sits near 1.5:1 instead, because a divider is not information and a divider at 3:1 outlines every card like a wireframe. The builder does not check and warn; it solves. If a value misses its floor, its lightness is moved until it passes, and the ratio shown beside each token is measured on the final value, not the intended one - a token with two jobs, like the primary, shows both of its measured pairs. The one exception is opt-in: lock the primary and the builder keeps your hex exact, solving everything around it and marking what the locked color itself cannot meet. 12 of the 14 tokens in each mode carry at least one verified pair, and the hover is additionally guaranteed a lightness step from the primary that you can actually see.
Which color becomes the primary?
The most saturated color in your palette, which is what people nearly always mean by the brand color. If that guess is wrong for your palette, click any color to lead with it instead: the first color wins ties. The choice travels in the address bar with the palette, so sending the link sends the same theme, lead and all.
Why did my brand color change slightly?
Because it had a job to survive. A very light or very saturated primary cannot carry a readable label or stand against a white card, so the builder holds your hue exactly, keeps as much saturation as the screen allows, and moves lightness the smallest amount that makes every floor pass. The change is usually a few percent, and the alternative is a button that fails an audit. If the color genuinely cannot move - a brand hex is a brand hex - lock it: the builder then keeps it byte-exact in both modes, solves everything around it, and marks the floors the locked color itself cannot meet instead of quietly fixing them. The lock travels in the link like everything else.
Why are primary and link separate tokens?
Because a fill and a sentence answer to different floors. A button fill needs 3:1 against its surface under the non-text rule, and your brand color can often be that fill exactly as it is - but inline link text needs 4.5:1, and a color that passes the first can fail the second. The link token is the primary hue taken the rest of the way, so both jobs pass without either compromising the other.
What are the success, warning and danger colors based on?
Fixed, recognizable hues - a green, an amber, a red - carried at your palette level of saturation, so a muted palette gets muted status colors rather than traffic lights, and then contrast-fitted like everything else. The three are also deliberately spread apart in lightness, checked against the same simulation our color blindness simulator runs: to red-green blindness, the most common form there is, green and red arrive as versions of one hue, and lightness is what still tells a success from an error.
How do I use the export?
The CSS export defines every token on :root for light, then redefines them under a data-theme="dark" attribute and a prefers-color-scheme media query, so it respects both a site toggle and the reader system setting. The Tailwind export is an @theme block for v4. The shadcn/ui export is a globals.css block in the shape the shadcn CLI scaffolds. JSON is for design tools and token pipelines. All four carry both modes.
Does this work as a shadcn/ui theme generator?
Yes, and the mapping is nearly one-to-one because shadcn names jobs the way this builder does: its border is our border, its input border is our border-strong, its ring is our focus, its primary and primary-foreground are our primary and its ink. The export emits :root, a .dark class and an @theme inline bridge, ready to paste over the matching blocks in globals.css - with one honest difference from most shadcn themes: every pair in it has already been contrast-verified, including the dark mode. The three neutral fills (secondary, muted, accent) share our tinted background, since this token set keeps one composed neutral ground rather than three.
All tools
Share