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 formats
Which notation to write colors in, how to convert between them and what happens at the edges of a display's gamut. Every other rule in this skill is stated perceptually and holds whatever notation you write.
Choosing a notation
Match whatever the project already uses. Notation is not a defect: a project on hex is not doing it wrong.
For a genuinely new color system, oklch() is the best default. Even lightness steps stay even, and a fixed hue stays fixed. See palette-generation.md [blocked].
Converting
Convert when the user asks, when an agreed migration is in scope or when the project is standardizing on a notation and this value is the straggler. Never convert an isolated value in a project that deliberately uses something else and never because this skill happened to load.
When conversion is in scope, change the values and nothing else:
- Leave CSS keywords alone:
currentColor,inherit,transparent,initial,unset. - Leave gradient functions alone. Convert the color stops inside them; do not touch the interpolation method.
- Leave colors in third-party configs that expect a specific format.
- Preserve comments and formatting.
Bulk conversion is a migration, not cleanup. It shifts every rendered color by a rounding margin and touches files nobody asked about, so it has to be the task rather than a side effect.
Gamut
Every sRGB color exists in Display P3, but not the reverse. The extra room is only at high chroma and varies by hue. At OKLCH L 0.65, green gains about 0.07 of chroma while blue gains about 0.015.
A color more vivid than its display can render gets gamut-mapped, which in current browsers mostly means clipped. It flattens neighbouring steps into one rendered color, so the top of a ramp can lose its distinctions on an sRGB screen. Maximum vividness varies by hue and lightness. Cyans top out far lower than reds and purples, so a clipping ramp clips at some steps and not others.
The fix is to reduce vividness while holding hue and lightness. Generate ramps against sRGB unless the product is display-restricted, and add P3 as an enhancement:
Order matters. The sRGB value comes first, so an sRGB screen shows a color you chose rather than one the browser clipped. The P3 rule overrides only where it will render.
For browser matrices predating oklch() support, the same layering works with @supports:
Check the project's actual browser matrix before adding this. On a modern baseline it is dead weight.
Modern CSS worth knowing
color-mix()derives one color from another, as incolor-mix(in oklab, var(--color-accent-solid) 15%, white)for a tinted background. Useful for states, but keep generated values out of the token layer, since a mixed color cannot be inspected in a design tool.- Relative color syntax adjusts one channel of an existing color.
oklch(from var(--color-accent-solid) calc(l - 0.1) c h)darkens the accent by hand. Powerful and easy to overuse, since a token defined by three chained derivations is unreadable. light-dark()puts both appearances in one declaration. See palette-generation.md [blocked].
All three resolve at render time. Compute the resolved value with a color library, or measure the rendered result, before reporting contrast.