// preview in
saved for every page

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 createBrand turns 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.

app/providers.tsx
<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:

minimal · light · #3fa34d
  1. 5019.4:1 text contrast
  2. 10017.2:1 text contrast
  3. 20014.5:1 text contrast
  4. 30011.2:1 text contrast
  5. 4008.3:1 text contrast
  6. 5006.2:1 text contrast
  7. 6004.7:1 text contrast
  8. 7006.7:1 text contrast
  9. 8009.6:1 text contrast
  10. 90012.7:1 text contrast
  11. 95016.1:1 text contrast

Each step becomes three tokens, plus --cml-brand with the color you passed:

TokenWhat it is
--cml-brand-600The step's color
--cml-brand-600-fgBlack or white, whichever reads better on it
--cml-brand-600-hoverThe 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:

scripts/brand.ts
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(" ")} }`;
CAREFUL

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 default600 / 400
cupertino600 in both modes
neubrutalism500 in both modes
material600 / 300
progressive, y2k600 / 500
retro800 / 600
editorialthe 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 · light · #3fa34d

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:

ColorWhy it is hard
Blue #2563ebThe default: black text up to step 500, white from 600 (5.3:1)
Yellow #facc15Very light hue: step 600 is a dark gold (#866c00) and yellow text on white would fail
Cyan #22d3eeLight and saturated: white on its step 600 is 4.86:1, the closest of the five to the floor
Coral #ff7f50Black and white read almost the same around steps 500 and 600 (5.35 and 5.46:1)
Gray #808080No chroma at all, so the scale is pure greys
TIP

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> with styleScopeProps)
  • 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
Was this page helpful? · Edit this page on GitHub