better-colors
OKLCH color space for web projects. Convert hex/rgb/hsl to oklch, generate palettes, check contrast, handle gamut boundaries, and theme with Tailwind v4. Triggers on oklch, color conversion, palette generation, contrast ratio, gamut, display p3, design tokens, hue drift, chroma, dark mode colors.
Install
npx skills add https://github.com/jakubkrehel/skills --skill better-colorsColor usage
Deploying color once the system exists: meaning, emphasis, gradients and appearance variants. For picking values see palette-generation.md [blocked], for naming them token-naming.md [blocked], for checking pairs contrast.md [blocked].
One color, one meaning
Users read a near-miss in hue as a slightly different shade, not as a different color.
The rule runs both ways. A color must not be absent where its meaning occurs. If the accent means interactive, an interactive element rendered neutral is just as misleading.
Use tokens in their role
A value that happens to work in two roles stops working in both at the next theme change.
The role inventory in token-naming.md [blocked] is the list of roles a system needs.
One colored action per view
Selected states may use the accent on the glyph and label. An active tab or a checked segment is state, not emphasis.
Gradients
The interpolation space is a look, not a correctness setting. Three are worth knowing, and the difference between them is most visible in the middle of the gradient:
oklab and sRGB are rectangular, interpolating in a straight line through the color space. oklch is polar, interpolating the hue angle, so it arcs around the wheel through every hue between the stops. That is why it stays saturated and why it can produce hues nobody asked for. A blue-to-pink gradient routes through purple, which is either the look or a surprise.
The gray dead zone is a rectangular-space problem. Two hues on opposite sides of the wheel sit either side of the neutral axis. A straight line between them passes near gray, and the middle goes lifeless. Either switch to a polar space, which routes around the axis, or add a third stop between the two and keep the space you have.
With a polar space you also control which way it goes around:
Banding shows up on large areas. A gradient spanning a hero with little contrast between its stops steps visibly on 8-bit displays. Widen the contrast, shrink the area or overlay a subtle noise texture.
Keep text off gradients where you can. Contrast varies continuously across one, so a single measurement does not describe it. Where text must sit on a gradient, measure the worst region rather than the average, or put a scrim behind it.
Color across cultures
Where a color carries meaning in finance, status or alerts, verify the meaning holds in every locale you ship to. Chinese financial UIs show gains in red and losses in green, the reverse of English ones. Where the product ships to such markets, make gain and loss per-locale tokens rather than hardcoded values.
Light, dark and increased contrast
When the product ships a dark appearance, every custom color needs a dark variant, derived per palette-generation.md [blocked]. Users who enable increased contrast expect visibly stronger differentiation in each appearance:
A single prefers-contrast block shared by both appearances darkens the color on a dark background too, lowering contrast there. The increased-contrast variant widens the gap to its background by at least 0.15 of OKLCH L over the default. Remeasure it, aiming for Lc 90 on body text. Widening the gap without remeasuring is not fixing it.