Brand
A customer's colour, made legible by construction — and now, made accountable.
A brand colour is an input, not a token
Drop a customer's hex straight into --primary and you break the thing Azalea exists to guarantee. Hana's own teal failed WCAG AA at 2.64:1 against white, and that was a colour somebody had at least looked at. A colour picked off a logo by whoever set up the account will fail more often than not.
brandTokens derives the whole set — the fill, the label, the hover step and the focus ring — and tests/theme.test.ts holds the invariant for every colour in the cube rather than for the ones somebody tried.
The half that was not guaranteed
ensureLegible promises the label on the button. It said nothing about the focus ring — which is drawn on the page, not on the fill, and answers to a different bar: 3:1, because a ring is a non-text indicator rather than text.
The same defect, left open one level down
…and a rounding bug behind it
Both correction loops worked in floating point and shipped toHex() of the result, which rounds. A colour that cleared the bar by a hair-width could round back under it: 34 colours passed the loop and failed once written down. Both loops now judge the quantised value, because quantised is what ships.
Found by the sweep, not by review: the first version of the fix passed every colour I thought to try and failed 34 of the 4,913 in the cube.
Try a colour
Every number below is computed in your browser from the colour in the box — the same functions the server uses to emit the stylesheet. Paste a customer's hex and see what will actually ship.
Azalea will adjust this
light
adjusted 0%- Requested
- #00cc00
- Applied
- #00cc00
- Label
- #0c1412 · 8.57:1
- Focus ring
- #02a704 · 3.06:1
- The focus ring is a separate colour: on the page it needs 3:1, which the fill does not reach.
dark
adjusted 0%- Requested
- #2ed52e
- Applied
- #2ed52e
- Label
- #0c1412 · 9.49:1
- Focus ring
- #62e062 · 11.09:1
- The focus ring is a separate colour: on the page it needs 3:1, which the fill does not reach.
Why report it at all
brandTokens will move a colour it cannot make legible, and until now it did so silently: a customer sets #008888, something deeper ships, and there was nowhere in the product, the logs or the docs that said so. The first anybody hears of it is a designer holding a brand book next to a screen.
Nudging the colour is still right — the alternative is shipping a button nobody can read. Doing it without saying so is not.
Guidance
Use it when
- brandAudit in the settings form where a tenant picks their colour, so they are told before they save.
- The notes verbatim. They say which requirement forced the change, which is the only useful part.
- report.exact to decide whether to say anything at all — most colours need nothing.
Reach for something else when
- Rejecting a colour that fails. Adjusting it keeps the brand recognisable; refusing it sends the customer to support.
- Reading the env in Azalea. Where a tenant's colour comes from is the app's business — BrandTheme takes it as a prop.
- Assuming the fill is the problem. It usually is not: the label can flip to ink, and it is the ring on the page that cannot.
API
| Prop | Type | Default | Description |
|---|---|---|---|
| brandAudit(hex) | BrandReport | — | What will ship, per theme, and what had to change. Never throws. |
| brandTokens(hex, { dark }) | BrandTokens | — | The four custom properties for one theme. |
| brandCss(hex) | string | — | All three theme states as :root overrides. What BrandTheme emits. |
| ensureLegible(rgb, target?) | Rgb | — | Nudge until the best label clears 4.5:1 on the fill. |
| ensureVisible(rgb, bg, target?) | Rgb | — | Nudge until it clears 3:1 against the page. The ring's rule. |
| PAGE_BACKGROUND | { light, dark } | — | The grounds the ring is measured against, kept in step with tokens.css by test. |