{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreidaw2uqvxflz6hwy7bh4ffgdwomcm5zkcythtvotlz3jux42msngq",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3moxqkgmkcty2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreibtmgrcskkvmllnvckc5es3cdl3ppsjfxbdt3tsrtb74wzlacqdbe"
},
"mimeType": "image/webp",
"size": 314210
},
"path": "/hasansarwer/you-finish-the-ui-run-lighthouse-and-suddenly-six-color-pairs-fail-wcag-aa-3jfo",
"publishedAt": "2026-06-23T15:22:48.000Z",
"site": "https://dev.to",
"tags": [
"css",
"webdev",
"javascript",
"ai",
"learn.esalt.net/salt-theme-gen"
],
"textContent": "Accessibility audits usually happen late. A tool runs on the deployed site, flags a dozen low-contrast text/background combinations, and now you're backtracking through component styles to fix values that were baked in months ago.\n\n`salt-theme-gen` runs 18 WCAG 2.1 contrast ratio checks at token generation time — before any components are built. If a token combination fails, you know before writing a line of CSS.\n\n## What gets checked\n\n\n const theme = generateTheme({ preset: 'ocean' });\n const { accessibility } = theme.light;\n\n\nEvery check is a `{ ratio: number; level: 'AAA' | 'AA' | 'fail' }` object:\n\n\n\n accessibility.primaryOnBackground\n // { ratio: 4.51, level: 'AA' }\n\n accessibility.textOnBackground\n // { ratio: 18.4, level: 'AAA' }\n\n accessibility.onPrimaryOnPrimary\n // { ratio: 4.9, level: 'AA' }\n\n\n18 checks + OKLCH auto-correction + CI-friendly report:\n\n**Text legibility** (2):\n\n * `textOnBackground` — body text on page background\n * `textOnSurface` — body text on card/input surfaces\n\n\n\n**Brand colors on background** (4):\n\n * `primaryOnBackground` — primary accent as text/icon on background\n * `secondaryOnBackground` — secondary accent as text/icon on background\n * `tertiaryOnBackground` — tertiary accent as text/icon on background\n * `quaternaryOnBackground` — quaternary accent as text/icon on background\n\n\n\n**On-color text — button/badge labels** (4):\n\n * `onPrimaryOnPrimary` — text inside a primary button\n * `onSecondaryOnSecondary` — text inside a secondary button\n * `onTertiaryOnTertiary` — text inside a tertiary button\n * `onQuaternaryOnQuaternary` — text inside a quaternary button\n\n\n\n**Semantic colors on background** (4):\n\n * `dangerOnBackground`, `successOnBackground`, `warningOnBackground`, `infoOnBackground`\n\n\n\n**On-semantic foregrounds** (4):\n\n * `onDangerOnDanger`, `onSuccessOnSuccess`, `onWarningOnWarning`, `onInfoOnInfo`\n\n\n\nAll built-in presets pass WCAG AA for every check. Many pass AAA.\n\n## Fail fast in CI\n\nThe accessibility report is just JavaScript — you can assert on it anywhere Node.js runs:\n\n\n\n import { generateTheme } from 'salt-theme-gen';\n\n const theme = generateTheme({ preset: 'ocean' });\n\n const failures = Object.entries(theme.light.accessibility)\n .filter(([, check]) => check.level === 'fail')\n .map(([name, check]) => `${name}: ${check.ratio.toFixed(2)} (needs 4.5)`);\n\n if (failures.length > 0) {\n console.error('WCAG AA failures in light mode:');\n failures.forEach(f => console.error(' ·', f));\n process.exit(1);\n }\n\n console.log('✓ All WCAG AA checks pass');\n\n\nAdd to `package.json`:\n\n\n\n {\n \"scripts\": {\n \"check:a11y\": \"node scripts/check-accessibility.mjs\"\n }\n }\n\n\nOr add it as a pre-build step: if accessibility fails, the build fails.\n\n## What happens when you use a custom color\n\nWhen you pass a hex color instead of a preset, `salt-theme-gen` still runs the checks:\n\n\n\n const theme = generateTheme({ primary: '#f59e0b' }); // amber\n const { accessibility } = theme.light;\n\n // Check automatically\n if (accessibility.primaryOnBackground.level === 'fail') {\n // amber on white fails — but the package already handled it:\n // OKLCH lightness was auto-adjusted to meet AA\n }\n\n\nWhen a generated color would fail AA contrast, `salt-theme-gen` auto-corrects it by shifting OKLCH lightness while preserving hue and chroma. You get the closest color to your input that still passes — without hand-tuning.\n\n## Checking dark mode too\n\nDark mode needs its own accessibility verification — colors that pass in light mode can fail in dark:\n\n\n\n const lightFailures = Object.entries(theme.light.accessibility)\n .filter(([, c]) => c.level === 'fail');\n\n const darkFailures = Object.entries(theme.dark.accessibility)\n .filter(([, c]) => c.level === 'fail');\n\n const allClear = lightFailures.length === 0 && darkFailures.length === 0;\n\n\nAll built-in presets pass in both modes.\n\n## Manual contrast check\n\nFor colors outside the token system — an illustration, a chart color, a third-party component:\n\n\n\n import { contrastRatio } from 'salt-theme-gen';\n\n const ratio = contrastRatio(\n theme.light.colors.primary,\n theme.light.colors.background\n );\n\n console.log(ratio); // e.g. 4.82\n console.log(ratio >= 4.5); // true — passes AA\n console.log(ratio >= 7.0); // false — doesn't reach AAA\n\n\nUse this when you want to verify a color combination that isn't in the standard 18 checks.\n\n## The bottom line\n\nColor contrast in design tokens can be handled before components are built. Running 18 WCAG checks on every theme means:\n\n * A junior developer can change the preset and know whether it passes\n * CI catches contrast regressions before they ship\n * Dark mode is verified separately, not assumed to be fine\n\n\n\nFull documentation: learn.esalt.net/salt-theme-gen\n\n_Part of the **salt-theme-gen — Design Tokens for Every Framework_ * series · Article 3 of 24*",
"title": "You finish the UI, run Lighthouse, and suddenly six color pairs fail WCAG AA"
}