How the contrast ratio got stuck in 2008.
The number every audit holds you to was fixed in a 2008 formula. Here is the legacy baked into it, what APCA changes, and how Shadl scores both on every pair.
Open the toolIn December 2008, WCAG 2.0 fixed the contrast ratio at (L1 + 0.05) / (L2 + 0.05), and the 0.05 in that expression is a viewing-flare constant carried over from research on screens in lit rooms. Every accessibility audit you have ever failed traces back to that one line of math.
The formula works. It is also a product of its moment, and several of its decisions look strange once you know where they came from. Shadl scores palettes with this exact formula and with APCA, the algorithm built to replace it, so it helps to know which parts of the 2008 design still bind you and which parts are already on their way out.
Relative luminance in WCAG is a weighted sum of linearized red, green, and blue: 0.2126 R + 0.7152 G + 0.0722 B. Those weights come straight from Rec. 709, the HDTV color standard, by way of sRGB.
The 0.05 added to both luminances is the interesting part. It models viewing flare, the small amount of ambient light that bounces off any real screen and lifts the darkest black you can perceive. Without it, pure black over pure black would divide by zero. With it, the ratio tops out at 21:1, which is (1 + 0.05) / (0 + 0.05), instead of running to infinity. That ceiling is the reason you never see a contrast score above 21 anywhere.
One more fossil lives in the linearization step. WCAG's luminance definition uses a breakpoint of 0.03928 to switch between the linear and gamma segments of the sRGB curve, while the published sRGB standard, IEC 61966-2-1, later settled on 0.04045. The gap changes results far below the precision anyone reports, but the older number still sits in the normative text, copied forward through every reimplementation for more than fifteen years.
The thresholds are not arbitrary, though they are not obvious either. WCAG's own Understanding document explains that 4.5:1 was chosen to preserve a 3:1 ratio for a person with roughly 20/40 vision, because that level of acuity loss costs about a factor of 1.5 in contrast sensitivity, and 3 times 1.5 is 4.5. The stricter AAA value of 7:1 runs the same arithmetic for 20/80 vision.
Large text gets a lower bar, 3:1 for AA, because bigger letterforms carry more of their own contrast. WCAG draws the line at 18pt, which is 24px, or 14pt bold, which is 18.66px. That oddly specific 18.66 is just 14 points converted to CSS pixels at 96 DPI, a unit conversion that escaped into the spec and never left.
Here is the decision that aged worst. The 2008 formula is symmetric: it divides the lighter luminance by the darker one, so it returns the same number whether you put dark text on a light background or light text on a dark one.
Human vision does not work that way. Light text on a dark field and dark text on a light field at the same nominal ratio do not read as equally legible, and the gap widens in exactly the thin weights and small sizes that dark interfaces lean on. For years this barely registered, because most of the web was dark-on-light and the formula was being asked the question it was tuned for. Then dark mode shipped across operating systems and design systems at once. Suddenly the same formula was passing pairs that looked muddy on a dark canvas and flagging pairs that looked perfectly crisp. Designers noticed, filed the bug against their own eyes first, and only later against the math.
That mismatch is the whole reason a replacement exists.
APCA, the Accessible Perceptual Contrast Algorithm, grew out of Andrew Somers' work starting around 2019 as a candidate for the next generation of the guidelines. It is polarity-aware: it reports a signed lightness contrast, written Lc, and scores light-on-dark and dark-on-light separately. Its rough body-text target is Lc 75, with Lc 60 for larger text and Lc 45 for large or bold display type.
APCA rides inside WCAG 3.0, whose First Public Working Draft landed on 21 January 2021 under a new Bronze, Silver, and Gold conformance model. Years later it remains a Working Draft. WCAG 2.1, published in June 2018, added a 3:1 rule for non-text elements but left the text ratio math untouched, and WCAG 2.2 in October 2023 did not change contrast at all. The situation today is genuinely split: the legally cited standard runs on math from 2008, and the more accurate math is not yet normative.
| Year | Release | What it left behind |
|---|---|---|
| 1996 | sRGB (IEC 61966-2-1) | The luminance weights and gamma curve the ratio still uses |
| 2008 | WCAG 2.0 | The (L1+0.05)/(L2+0.05) ratio, the 0.05 flare term, the 21:1 ceiling |
| 2018 | WCAG 2.1 | Adds 3:1 for non-text (SC 1.4.11); same ratio math for text |
| 2019 | APCA / SAPC begins | Polarity-aware Lc, built to fix the symmetry problem |
| 2021 | WCAG 3.0 First Public Working Draft | APCA, plus the Bronze, Silver, and Gold model, all non-normative |
| 2023 | WCAG 2.2 | No contrast change; the 2008 math remains the law |
Shadl puts both systems on every pair on purpose. The WCAG ratio is what an auditor will quote and what the European Accessibility Act, in force since 28 June 2025, points to through EN 301 549. The APCA Lc is the better predictor of whether the text is actually comfortable to read, especially in dark mode.
When we built the scoring, the check we refused to ship without was matching WebAIM's published contrast values to two decimal places, because this audience cross-checks against WebAIM the moment a number looks off. For APCA we call APCAcontrast() from the apca-w3 reference package directly rather than reimplementing it, since the algorithm is still being tuned and an in-house copy would quietly drift away from the source.
Ship the WCAG-passing version for compliance, and treat a strong APCA Lc as the signal that a pair will be comfortable to read rather than merely lawful. When the two disagree, the disagreement is usually telling you something real about polarity, and that is worth a second look before you lock the swatch.
4.5:1 for normal-size body text to meet WCAG 2.1 AA, and 7.0:1 for AAA. Large text (18pt+ or 14pt+ bold) needs 3.0:1 for AA and 4.5:1 for AAA. UI components and graphical objects need 3.0:1. Shadl labels every pair against these thresholds so you do not have to remember them.
APCA Lc 75 or higher for body text under 18px, Lc 60+ for large text or bold body, and Lc 45+ for non-text UI components. APCA accounts for polarity, so light-text-on-dark and dark-text-on-light score separately. Shadl shows the absolute Lc value at integer precision next to each pair.
Ensure every body-text pair hits 4.5:1 and every large-text pair hits 3.0:1. Shadl's coaching note tells you exactly which swatch to nudge ('darken text by 6% L' or 'lighten bg by 4% L') when a pair fails. Adjust the OKLCH lightness in the suggested direction and re-check until all badges read AA or AAA.
Click the export panel and pick Tailwind v4 @theme. Shadl emits an OKLCH-native @theme block you paste directly into your global stylesheet, with --color-bg, --color-surface, --color-text, --color-primary, and --color-accent already mapped to your locked swatches. Tailwind v3 config (hex), CSS custom properties, and JSON tokens are also one-click exports.
Lighthouse runs the same WCAG 2.1 contrast formula Shadl uses, so failures almost always come from text/background or interactive-element pairs under 4.5:1 (or 3.0:1 for large text). Paste your palette into Shadl, find which pair is failing, and follow the coaching nudge to lift it above the threshold. The numbers Shadl reports match WebAIM and Lighthouse to two decimals.
WCAG 2.1 is the legal floor today (EAA, ADA, Section 508). APCA is in WCAG 3 drafts and models perceptual contrast more honestly, especially for very light or very dark backgrounds. Shadl shows both. If they disagree, ship the WCAG-passing version until WCAG 3 is normative.
No. Spacebar is suppressed whenever an input, textarea, or contenteditable region has focus. The Generate button always works as a fallback.
Yes, if those tools also use the apca-w3 reference package. Shadl calls the canonical implementation directly and displays the Lc value at integer precision. If another tool shows a significantly different number, it may be using an older or unofficial APCA formula.
Shadl runs entirely in your browser. No data is sent to any server. Your palette values live only in the URL and localStorage. Full privacy policy.
Start building a palette