{
  "$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"
}