Brand color
Pass one color and get an 11-step scale with readable text on every step. Each skin decides where the brand shows up.
// you'll learn
- How
createBrandturns one color into 34 tokens - Why text on the brand is always readable, for any color you pick
- How the same color lands in different skins, and which colors are hardest
One prop
Set brand on a StyleProvider to any CSS color: hex, rgb(), oklch(), a named color. Leave it out and it inherits from the parent provider; at the top, the default is blue #2563eb.
<StyleProvider styleName="minimal" brand="#3fa34d">
<App />
</StyleProvider>
The brand bar at the top of this site does the same for every preview on the page. Pick a color there and come back.
The 11-step scale
createBrand (from @camaleo/styles) converts the color to OKLCH, keeps its hue and builds steps 50 to 950 at fixed lightness, from 0.97 down to 0.24. Light steps take a small share of the color's chroma, the middle steps all of it; anything outside sRGB is clamped back in. Try a color:
- 5019.4:1 text contrast
- 10017.2:1 text contrast
- 20014.5:1 text contrast
- 30011.2:1 text contrast
- 4008.3:1 text contrast
- 5006.2:1 text contrast
- 6004.7:1 text contrast
- 7006.7:1 text contrast
- 8009.6:1 text contrast
- 90012.7:1 text contrast
- 95016.1:1 text contrast
Each step becomes three tokens, plus --cml-brand with the color you passed:
| Token | What it is |
|---|---|
--cml-brand-600 | The step's color |
--cml-brand-600-fg | Black or white, whichever reads better on it |
--cml-brand-600-hover | The step moved 0.09 in lightness, away from its text color |
You can call it yourself, for example to write the tokens into a static CSS file at build time:
import { createBrand } from "@camaleo/styles";
const vars = createBrand("#3fa34d");
// { "--cml-brand": "#3fa34d", "--cml-brand-50": "#…", "--cml-brand-50-fg": "#000000", … }
const css = `:root { ${Object.entries(vars).map(([k, v]) => `${k}: ${v};`).join(" ")} }`;
createBrand throws on a string it can't parse. If the color comes from users, validate it first (or catch the error and fall back to a default).
How contrast is guaranteed
Text on a step is never "the brand's text color": it is black or white, picked per step by WCAG contrast. Against any color, the better of black and white reaches at least 4.58:1, so every -fg passes 4.5:1, whatever the input. Hover colors move away from that text color, so hovering never lowers contrast.
Skins don't use the raw scale; they map it to semantic tokens and pick the steps that work on their surfaces:
| Skin | --cml-accent (light / dark) |
|---|---|
| minimal, and skins that keep the default | 600 / 400 |
| cupertino | 600 in both modes |
| neubrutalism | 500 in both modes |
| material | 600 / 300 |
| progressive, y2k | 600 / 500 |
| retro | 800 / 600 |
| editorial | the ink color; the brand marks focus and selection |
Brand text on the page (links, the focus ring, info) uses --cml-accent-text, which is 700 in light mode and 300 in dark in the default skin. The matrix test checks that it stays at 4.5:1 on the surface, and that the focus ring and control borders stay at 3:1.
The same color in 3 skins
One brand, three skins: each one picks its own step, so a yellow that is a fill in one skin can be a dark gold in another. Switch the mode in the "preview in" bar too.
minimal
neubrutalism
material
Hard colors
Most colors are easy. These five are the ones that break naive palettes, and the matrix test runs every skin, in light and dark, with each of them:
| Color | Why it is hard |
|---|---|
Blue #2563eb | The default: black text up to step 500, white from 600 (5.3:1) |
Yellow #facc15 | Very light hue: step 600 is a dark gold (#866c00) and yellow text on white would fail |
Cyan #22d3ee | Light and saturated: white on its step 600 is 4.86:1, the closest of the five to the floor |
Coral #ff7f50 | Black and white read almost the same around steps 500 and 600 (5.35 and 5.46:1) |
Gray #808080 | No chroma at all, so the scale is pure greys |
If your brand is a light color like yellow, expect skins that use step 500 or 600 as a fill to show it darker than your logo. That is the price of readable text; use your exact color in illustrations and marketing, not in controls.
Before you ship it
- The brand is set once, on the top provider (or on
<html>withstyleScopeProps) - Colors that come from users are validated before they reach
createBrand - You change
--cml-accent, not a brand step, when you want another shade - You checked your color in light and dark, in every skin you ship