Deft
Ask AI

Search

Search skills, authors and topics

jakubkrehel / skills

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-colors

Contrast

Contrast is measured between a foreground color, meaning text, an icon or a UI element, and the background color it actually renders against, usually the nearest ancestor that paints one. Identify that background first. Measuring against the page background when the element sits on a card gives the wrong answer.

better-accessibility decides which requirement applies to a pair. The thresholds below are what a measured pair is reported against. Where neither that skill nor the project names a standard, report the WCAG 2 result and add the APCA value.

WCAG 2 thresholds

WCAG 2 is the standard formal conformance claims are made against.

Content typeAAAAA
Text4.5:17:1
Large text, at least 24px or 18.67px bold3:14.5:1
UI components and graphical objects3:1n/a

Disabled or inactive controls and logos are exempt.

APCA thresholds

Lc, Lightness Contrast, is APCA's measure of perceived contrast. These levels simplify APCA's full font-size and weight lookup table:

Content typeMinimum
Body text, columns or blocks of textLc 75, preferred Lc 90
Non-body text such as labels and headlinesLc 60
Large text, at least 36px regular or 24px boldLc 45
Fine-detail or outline iconsLc 45
Solid icons and UI componentsLc 30
Non-text that only has to be discernible, such as dividersLc 15

Disabled and placeholder text are not covered by a requirement. Aim for Lc 30 so they stay legible.

Lc is signed. Positive means dark text on a light background, negative means light text on a dark background. Compare the absolute value against the threshold.

Fix a failing pair only on request

Change lightness first. It is the channel contrast responds to. Hue and saturation move the measured value far less.

Move the foreground away from the background in perceived lightness, holding hue and saturation, then remeasure. Keeping hue fixed is what stops a contrast fix becoming a palette change.

css
/* Failing: text too close to its background in lightness (Lc ≈ 50) */color: #7d93b0;background: #eef2f7;
/* Fixed: darker text, same hue (Lc ≈ 88) */color: #2b3a4f;background: #eef2f7;

Two constraints on the fix:

  • Mid-lightness backgrounds cap what is achievable. On a background near OKLCH L 0.75, even pure black text reaches only about Lc 60. Body text needs a background near one extreme, so a mid-range background is the thing that has to change.
  • Pushing lightness can push the color out of gamut. Reduce saturation as needed to keep it renderable. See color-formats.md [blocked].

Always remeasure after changing a value. Do not assume a fix landed.

Pick text polarity from the background

Compare black and white using the project's required contrast metric. APCA and WCAG 2 can favor different text colors on the same background. A higher score does not by itself mean the pair passes.

WCAG 2's neutral crossover is near 56% OKLCH lightness. At oklch(65% 0 0), black measures about 6.49:1 and white 3.23:1. Only black passes AA for normal text. For colored backgrounds or other foregrounds, measure the actual pair rather than applying a crossover.

What to check

  • Every pair, in every appearance. APCA scores mirrored pairs differently, so a pair passing in light mode can fail in dark.
  • Translucent surfaces. A color on a backdrop-filter header or an overlay shifts with whatever scrolls behind it. Test against the lightest and darkest content it can sit over, or make the surface opaque enough that the shift cannot break the pair.
  • Computed colors. color-mix(), relative color syntax and opacity modifiers resolve at render time; measure the rendered result, not the declaration.
  • Text over images. There is no single background color. Measure the worst region, or guarantee one with a scrim.