Getting started
Theming with one seed color
A Monsoon theme is one line of CSS: --seed: #0d9488. The browser derives primary, secondary and accent colors, surfaces, borders, muted text and the focus ring from it, with foregrounds that keep readable contrast. No preprocessor and no generator website.
Set the seed
Any valid CSS color works: hex, a named color, rgb(), hsl() or oklch(). Set it on the root element to theme the whole page:
:root {
--seed: #0d9488;
}Load this after Monsoon, in your own stylesheet or in a <style> tag placed after the link, so it wins over the default. Without it, the default seed is oklch(0.55 0.22 262), a saturated blue. To experiment, add style="--seed: ..." to an element in the playground and watch the tokens change.
Every derived token
Each token is a custom property (var(--primary)) and a family of utilities (bg-primary, text-primary, border-primary, ring-primary and more). Lightness values are on the OKLCH scale from 0 (black) to 1 (white):
| Token | Light / dark lightness | What it is for |
|---|---|---|
background | 0.99 / 0.16 | The page background. Chroma is capped near zero, so only a hint of the seed hue remains. |
foreground | 0.21 / 0.96 | Body text on background and the surfaces. |
surface | 1 / 0.2 | Cards and panels. |
surface-2, surface-3 | 0.97, 0.94 / 0.24, 0.28 | Raised or nested layers: table headers, wells, hover fills. |
muted | 0.955 / 0.27 | Subtle fills such as tags and code backgrounds. |
muted-foreground | 0.47 / 0.74 | Secondary text, captions and placeholders. |
border | 0.9 / 0.33 | Hairlines. It is also the default color of every border, so border alone needs no color class. |
primary | the seed, moved out of 0.52 to 0.66 | Main actions and links. In dark mode the seed is lifted to at least 0.7 first. |
primary-foreground | 1 or 0.17 | Text on primary: white on a dark primary, near black on a light one. |
ring | same as primary | Focus rings, the default ring color and the focus outline in the reset. |
secondary | 0.94 / 0.3 | Quiet buttons and chips: a low chroma tint of the seed. |
accent | 0.74 to 0.88, both modes | Highlights: the seed hue turned by 180 degrees, kept light. |
success, warning, danger, info | fixed hues | Status colors at hues 150, 80, 27 and 235. They do not follow the seed. |
primary, secondary, accent and the four status colors each have a -foreground partner for text on top of them, muted pairs with muted-foreground, and foreground is the text color for background and the surfaces. The 22 hue palette (bg-emerald-100, text-rose-600) is separate and fixed: it never follows the seed.
Theme one section
Custom properties are computed where they are declared, so the tokens must be declared again wherever the seed changes. Monsoon does that automatically for any element whose style attribute sets --seed:
<section style="--seed: #db2777" class="rounded-2xl bg-surface p-6 space-y-3">
<h2 class="text-xl font-bold text-primary">Spring sale</h2>
<button class="rounded-lg bg-primary text-primary-foreground px-4 py-2">Shop now</button>
</section>#0891b2
secondary#db2777
secondary#facc15
secondaryLook at the yellow island: a light seed gets near black text on its primary button, while the darker seeds get white text. Each island also tints its own surface and computes its own accent.
Setting the seed from your own CSS
If you set --seed from your stylesheet instead of the style attribute, add the theme class to the same element. It tells Monsoon to recompute the tokens there without painting anything:
[data-brand="ocean"] {
--seed: #0369a1;
}<div data-brand="ocean" class="theme rounded-2xl border bg-surface p-6">
<a href="/plans" class="text-primary font-medium">See plans</a>
</div>Override a single token
Sometimes the brand needs one exact color. Set that token yourself after Monsoon loads. Use the same list of selectors Monsoon uses for its tokens, so the override also applies inside themed sections:
:root, .theme, .dark, .light, [data-theme], [style*="--seed"] {
--danger: #c81e1e;
}The partner token follows automatically: --danger-foreground is computed from var(--danger), so it still flips between white and near black for your color. The same is true for primary, secondary, accent and the status colors.
To give an override a different value in dark mode, use --mode, which is 0 in light mode and 1 in dark mode, the same way Monsoon does:
:root, .theme, .dark, .light, [data-theme], [style*="--seed"] {
/* lightness 0.5 in light mode, 0.72 in dark mode */
--info: oklch(calc(0.5 + 0.22 * var(--mode)) 0.13 250);
}An overridden color skips the lightness adjustment that Monsoon applies to the seed. Check its contrast against its foreground in both modes.
Accent: the opposite hue
The accent token takes the seed hue and turns it by 180 degrees, so it always sits on the opposite side of the color wheel: a teal seed gets a coral accent, a blue seed gets an amber one. Its lightness is kept between 0.74 and 0.88 and its chroma is capped at 0.16, so it reads as a bright highlight in both modes, always with dark text.
Use it sparingly, for things that should stand out from the primary color: a "new" badge, a highlighted price, the second stop of a gradient.
<div class="h-24 rounded-2xl bg-gradient-to-br from-primary to-accent"></div>Contrast guarantees
White text needs a dark fill and near black text needs a light fill. Around an OKLCH lightness of 0.52 to 0.66, neither choice reaches 4.5:1 for every hue, so Monsoon moves the primary color out of that band, to the side that is closer to your seed, and flips its foreground on the same threshold. The other foregrounds flip on the same rule.
The test suite checks a contrast ratio of at least 4.5:1 (WCAG AA for body text) for these 13 pairs, in light and dark mode, for 18 named seeds and a sweep of 2,197 seeds across sRGB:
primary,secondary,accent,success,warning,dangerandinfo, each with its-foreground,foregroundonbackground,surface,surface-2andsurface-3,muted-foregroundonbackgroundand onmuted.
The guarantee covers those pairs. Combinations you choose yourself, such as palette colors or text-accent on a white surface, are up to you to check.
Frequently asked questions
Which color formats can I use for --seed?
Any CSS color: hex, named colors, rgb(), hsl(), oklch() and others. The browser converts it to OKLCH for the calculations.
Can different parts of a page use different seeds?
Yes. Put style="--seed: ..." on any element, or set the seed with a class and add theme to that element.
Do the palette colors change with the seed?
No. The 22 hue palette is fixed. Only the semantic tokens follow the seed.
What happens in browsers without relative colors?
They get static tokens computed from the default seed, with primary and ring set to your seed directly. See Browser support.