Color / OKLCH
The OKLCH-based colour system — violet brand, stone neutral, four status palettes.
Every colour in Clarity by Nivoda is authored in OKLCH and stored with a hex fallback. The source of truth is packages/tokens/src/color/primitive.tokens.json; semantic roles (action.primary, status.success) map onto it in semantic.tokens.json.
Why OKLCH
oklch(L C H) is perceptually uniform — a ten-point shift in lightness looks like the same perceived step whether the hue is violet, green, or stone. HSL doesn't do this; a 50% lightness yellow is visibly brighter than a 50% lightness blue. OKLCH also reaches into the P3 wide-gamut space, so displays that support it render saturated hues (brand violet especially) with more chroma than sRGB can hold.
Math is meaningful: step a scale by fixed L intervals and the perceived contrast ladder is even. That's how the 50–950 ramps below were built.
Hex fallbacks are stored alongside because React Native can't parse OKLCH. Web emits oklch(...); native reads hex. One source file, two outputs.
Palette structure
Two layers:
- Primitive — raw hue scales, 11 steps each (50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950). Named after the hue (
violet,stone,green, etc.), not the role. - Semantic — role tokens that reference a primitive step.
action.primarypoints atviolet.600;surface.baseatstone.950in dark mode. Consumers should reach for semantic tokens; primitives are for designers building new roles.
Violet — brand
Brand hue. The only accent that implies Nivoda. Reserved for primary actions, focus rings, selection states, and the rare moment that needs to lift off the surface. Never chrome, never decorative.
violet.600 (#6330f5) is the primary action default. violet.500 (#7655fd) is the hover. violet.700 (#5620e1) is the pressed state. Everything else supports focus rings, tinted surfaces, and the rare decorative moment.
It's violet, not purple. The names are not interchangeable in source — reviews catch purple and send it back.
Stone — neutral
Warm neutral. Every surface, every border, every run of body text grounds on stone. Cool greys have been tried and rejected; they fight the luxury-dark direction.
stone.950 (#0c0a09) is Nivoda black — soft, warm, never pure #000000. Pure black is jarring on a luxury surface; stone.950 reads as dark and considered. Light-mode body text uses stone.900; dark-mode body text uses stone.50.
Status palettes
Four status hues, shown here as the three load-bearing steps (50, 500, 900). Full ramps are in the source file.
Green — success
#f3faf6#59b186#25362fConfirmations, completed states, positive deltas. Maps to status.success.
Red — error
#fef2f2#ef4444#7f1d1dErrors and destructive actions. Maps to status.error and action.destructive.
Blue — info
#eef4ff#326cff#192b8fInformational messages and in-body links. Maps to status.info.
Amber — warning
#fffbeb#f59e0b#78350fWarnings, holds, attention states. Maps to status.warning.
Hex and OKLCH side by side
Each primitive token carries both:
{
"$value": {
"colorSpace": "oklch",
"components": [0.515, 0.2644, 284],
"hex": "#6330f5"
}
}Web builds emit oklch(0.515 0.2644 284) into the CSS file. React Native reads hex. The two values aren't independent — hex is generated from the OKLCH triple at authoring time and checked into source so there's no runtime conversion cost on native.