CSS Garden All articles
Accessibility

Grey November Contrast: Palettes That Survive a Manchester Drizzle

CSS Garden
Grey November Contrast: Palettes That Survive a Manchester Drizzle

Photo: Ridiculopathy, CC BY-SA 4.0, via Wikimedia Commons

Manchester in November does not give you dramatic darkness. It gives you grey light: a flat, damp, bounced illumination that makes a laptop screen look like a smudge on a window. A palette can pass a contrast checker in a bright office and still feel unreadable on the 08:12 tram, in a council office with fluorescent tubes, or on a five-year-old ThinkPad with a washed-out panel. Contrast is not a checkbox. It is a reading condition.

The checker is a starting line

WCAG 2.2 AA asks for 4.5:1 for normal text and 3:1 for large text, with similar thresholds for UI components. Those numbers matter, especially for UK public sector work. The Public Sector Bodies (Websites and Mobile Applications) Accessibility Regulations 2018 put accessibility on the procurement agenda, and many councils, NHS trusts, and arm's-length bodies now expect WCAG 2.2 AA, EN 301 549 evidence, and an accessibility statement that reflects reality.

But a pass is not a promise. Relative luminance is calculated from linearised RGB channels, weighted roughly as 0.2126 red, 0.7152 green, and 0.0722 blue. That maths tells you how much light a colour emits relative to another. It does not know that your grey text is 14px, that the background is a subtle gradient, or that the user is reading on a glossy screen under a grey sky.

Build ramps in oklch()

Hex ramps are a trap because equal steps in hex are not equal steps in perception. Two greens that look close in code can look miles apart on screen, and a ramp that seems smooth on your monitor can collapse on a cheaper display. oklch() gives you lightness, chroma, and hue, so you can build a ramp with even perceived lightness.

@layer tokens { :root { --grey-100: oklch(96% 0.005 250); --grey-300: oklch(82% 0.008 250); --grey-500: oklch(62% 0.01 250); --grey-700: oklch(44% 0.012 250); --grey-900: oklch(24% 0.015 250); --text: var(--grey-900); --text-muted: var(--grey-700); --surface: var(--grey-100); } }

Then test the pairs you actually ship, not the pretty swatches in the brand PDF. Muted text on a raised card is a different pair from muted text on the page background. Disabled buttons are a different pair again. Focus rings are a different pair, and they need to work against both light and dark surfaces.

The bright-monitor trap

Designers often test on a calibrated display in a room with controlled lighting. That is useful for colour decisions, but it is not the whole audience. A low-quality laptop panel may have poor viewing angles, a narrow gamut, and a matte coating that turns dark greys into muddy browns. A phone outdoors in winter is another environment again. Test at 50% brightness, at an angle, and on the least glamorous screen in the office. If the contrast only works on your studio monitor, it does not work.

Also test with browser zoom at 200% and with increased default font size. Text that passes at 16px can fail when it reflows into a narrow column with a background image behind it. Contrast is a property of the rendered page, not the token file.

Forced colours and prefers-contrast

Windows High Contrast and similar forced-colors modes override your palette. If your component depends on a background image, a gradient, or a custom focus ring, it can disappear. Use system colours for critical UI and test with forced colours enabled.

@media (forced-colors: active) { .button { border: 1px solid ButtonText; forced-color-adjust: none; } }

Be careful with forced-color-adjust: none; it is a sharp tool. Usually you want the browser to adjust colours, and you want your borders, icons, and focus states to remain visible. For users who ask for more contrast, prefers-contrast: more is a useful hint, though support and behaviour vary.

@media (prefers-contrast: more) { :root { --text-muted: var(--grey-900); --border-subtle: var(--grey-700); } }

A reusable contrast audit checklist

None of this replaces a proper accessibility audit, but it catches the everyday failures that make a site feel hostile on a wet Tuesday. Good contrast is not about passing a checker. It is about respecting the light your reader is actually sitting in.


All articles

Related Articles

Allotment Rules for Design Tokens: Grow Patches, Not a Monoculture

Allotment Rules for Design Tokens: Grow Patches, Not a Monoculture

The Native CSS Toolbox: Twelve Features That Shrink Your Sass Build

Specificity Debt: How Your CSS Cascade Became a Weed-Choked Mess

Specificity Debt: How Your CSS Cascade Became a Weed-Choked Mess