dd_wcag 0.5.0 Contrast Checker and Palette Builder

dd_wcag 0.5.0 Contrast

Checker and Palette Builder

By Jared Lyvers, ldnddev — August 24, 2026

What 0.5.0 actually changes

When we first shipped dd_wcag, it was a terminal pair checker: paste a foreground and a background, get a WCAG ratio, keep working. The v2 update added APCA Lc next to that ratio so you could see compliance and perceptual readability at the same time. That was the right next step. It was a checker.

dd_wcag 0.5.0 is the jump from checker to daily design-system tool. You still test one pair against WCAG 2.x and APCA. You can now set the WCAG bar to AA or AAA and the APCA pass bar to Lc45, Lc60, Lc75, or Lc90. Size runs 6 to 120px. Weight is a real 100–900 stepper, not a bold toggle pretending to be typography. When a pair fails, a Fix pane nudges OKLab lightness toward a passing candidate instead of sending you back to a color picker to guess. And when the brand colors are settled, the Palette tab generates a WCAG-gated _palette.scss from Primary, Secondary, Tertiary, and an optional Support color and the same token families the ldnddev Framework already consumes.

That last part is the point. Accessibility color work fails in production when the check lives in one tool and the tokens live in another. 0.5.0 keeps the check, the fix, and the export in one TUI so the file you drop into a theme is the file you already audited. If you design systems, write component CSS, or ship Framework-based Drupal and WordPress sites, this release is for you.

Why we still run WCAG and APCA together

WCAG Success Criterion 1.4.3 is still the bar most audits, RFPs, and legal reviews use. Normal text needs 4.5:1. Large text, roughly 18px and up, or 14px and bold can pass at 3:1. AAA tightens that to 7:1 and 4.5:1. Those numbers are not going away this year, and they should not. If you only optimize for a newer algorithm, you can still fail a procurement checklist.

The problem is that a ratio is not how people read. WCAG 2.x contrast math is a luminance ratio. It treats a pair the same whether the text is 12px or 24px, and it treats “light text on a dark surface” the same as the reverse. On modern displays, especially in dark UI, that math can pass pairs that feel thin and fail pairs that look fine. APCA scores lightness contrast as an Lc value that tracks size, weight, and polarity. Lc 75 is a practical minimum for body copy; Lc 90 is preferred for columns of text; Lc 60 is closer to UI labels; Lc 45 is for large, heavy type. A negative Lc means light text on a dark background. That direction matters. “Text on Primary” is not the same pair as “Primary on Text.”

The useful workflow is the unglamorous one: use APCA to judge whether people can actually read the type, and keep WCAG as the compliance failsafe so the same pair does not fail an audit. 0.5.0 makes that the default view. Official WCAG pass/fail uses AA or AAA plus large-text rules from size and weight. Official APCA pass/fail is whether absolute Lc meets the header bar you chose. A lookup line still recommends Lc for the current size and weight, but it is advisory. Quick-reference rows at 12, 14, 16, and 18px (APCA also 24px) show how the same pair behaves when a caption, a body, and a heading share a color.

Info: APCA is not a drop-in legal replacement
APCA is a better model of readability on self-lit screens, and a candidate method for future WCAG work. Until a standard you are contracted to meet says otherwise, keep the WCAG ratio. 0.5.0 shows both so you do not have to pick a camp.

If your team has been arguing which number is real, stop arguing. Both numbers are real. They answer different questions. 0.5.0 puts the answers on one screen.

Check a pair the way you actually ship it

Most contrast tools assume 16px regular text and a hex field. That is a start. It is not how interfaces ship. A button label at 14px / 600, a card title at 22px / 700, and a helper caption at 12px / 400 can share a brand color and produce three different outcomes. 0.5.0 treats size and weight as first-class controls because they are first-class in CSS.

Enter foreground and background as hex, rgb() / rgba(), or hsl(). Live conversions sit under the form so you can copy whichever format your stylesheet wants. Style chips cover Regular, Bold, Italic, and Bold+Italic; italic is visual only and does not change the math. Weight steps 100 through 900. Size clamps at 6–120px. Tab commits a field.

The TUI preview is labeled "approx" on purpose because a terminal cannot render a real CSS pixel or load Roboto. Contrast math still uses the numeric size and weight you typed. For the sample you would actually show a stakeholder, press Ctrl+O. The app writes /tmp/dd_wcag_preview.html and opens it in the browser at true CSS size, with both WCAG and APCA in the meta block.

A pair we use in the illustrated tutorial makes the size issue obvious. #88D9F7 on #0F1114 — a light brand cyan on a near-black surface — passes WCAG at 11.99:1 and passes APCA around Lc 78 at 16px with the header bar at Lc75. Drop the same pair to the 12px lookup row and APCA fails. That is not a bug in the tool. Small, thin type needs more lightness contrast. If your design system uses that cyan for captions, 0.5.0 will tell you before a QA ticket does.

Swap foreground and background with Space. Copy the focused hex with Ctrl+C, or Shift-click a swatch. Cycle the WCAG and APCA targets in the header when you are auditing to AAA, or checking UI chrome against Lc45 instead of body copy against Lc75. The numbers do not change when you move the bar. The pass/fail glyphs do.

When a pair fails, Fix it instead of guessing

Brand palettes fail in predictable ways. Two mid-chroma brand colors stacked as text-on-fill is the classic. Coral #F98971 on gold #FFCA76 lands around 1.58:1 and Lc 18. Both engines agree: do not use that pair for text. The old workflow was to drag a lightness slider in a separate app, paste back, and hope. 0.5.0 keeps you in the same session.

Press Ctrl+F or click Fix. On a wide terminal the pane is a strip along the bottom. On a narrow terminal it is a centered overlay. You get the current pair, a nearby candidate, OKLab lightness gauges for foreground and background, and Apply / Next / Close. The search keeps hue by default and walks lightness on the focused axis until the header WCAG target and the header APCA bar both pass, or the bounds run out. If only one metric clears, the candidate still shows as mixed and you can still apply it. Perfect both is the goal. Blocking apply until perfect both is how people abandon the tool.

This is the part we care about for design systems: Fix will not invent a new text color to force a pass. Text roles in the palette are fixed tokens. If the pair is “Text on Primary,” only Primary may move. That matches how a real theme should work. You do not rewrite body text to rescue a too-light brand fill. You adjust the fill until the existing text role passes, or you pick a different surface.

From Fix you can send the focused value into a palette role of Primary, Secondary, Tertiary, or Support without overwriting the Contrast pair. That handoff is how a rescued brand color becomes a token instead of a one-off hex in a ticket. Nudge lightness with the gauges or with [ and ]. Ctrl+N walks the next candidate.

Ship accessible color before the audit

If contrast still shows up as a late QA finding, we can help you lock WCAG and APCA into the tokens your design system actually ships.

Turn brand colors into a gated _palette.scss

Checking one pair is how you debug a component. Shipping a system is how you stop debugging the same component next month. The Palette tab is the 0.5.0 feature that makes dd_wcag more than a nicer contrast calculator.

Press 2. Required bases are Primary, Secondary, and Tertiary. Support is optional and defaults to #46BE8C if you leave it alone. Text is a read-only row. Generation never edits it. Enter the brand hexes you already use — the Framework defaults of #88D9F7, #FFCA76, and #F98971 are a fair first run if you want to see a known-good set — then press Ctrl+G.

Generate derives light and dark surfaces, strong and subtle variants, action states (default, hover, pressed, disabled with surface, text, and border), semantic colors, neutrals, and support tokens for overlay, border, and focus. A successful run on those brand bases produces on the order of 140 tokens and a compliance pass with zero blocking failures. The detail list is scrollable. You can inspect every variable and every check instead of trusting a green badge.

Export is gated on purpose. Ctrl+S opens a file picker defaulting to _palette.scss. Ctrl+C copies the same SCSS if a clipboard helper is available. Both refuse if you have not generated yet, if a required base will not parse, or if blocking WCAG failures remain — action text on action surface, borders, focus rings. Disabled-state issues are advisory and do not block. That split is intentional. Disabled chrome should look inactive. It should not hold up a palette that otherwise passes. Generated values are opaque rgba(r, g, b, 1), grouped in a fixed order so diffs stay readable when you regenerate after a brand tweak.

The pairing matrix is the other half of the tab. Rows are text roles, columns are surfaces. Glyphs show whether WCAG and APCA both pass, disagree, or fail. Double-click a failing cell to open Fix for that pair. APCA is directional here too, which is why the matrix is not a symmetric grid you can skim on autopilot. If Secondary-on-Primary fails and Primary-on-Secondary passes, that is a real design constraint, not a display quirk.

Built for the ldnddev Framework token set

This is not a generic “export some CSS variables” feature. The generated names match the Framework contract: $c_primary_default, $c_primary_strong, $c_primary_subtle, and the --dark variants; the full $c_primary_action_* state set; the same pattern for Secondary and Tertiary; fixed text roles such as $c_text_primary and $c_text_inverse; support tokens for overlay, border, and focus. If you already build pages with dd-cta, dd-alert, dd-accordion, and the rest of the Framework, those components already read this vocabulary.

That is why we aimed 0.5.0 at accessibility and design-system people first, and at Framework developers in the same breath. A design-system lead can sit in the TUI, prove that body text on a brand surface clears AA and Lc75, fix the pair that does not, and hand engineering a file instead of a screenshot. A Framework developer can drop _palette.scss next to the existing variables, regenerate after a client rebrand, and keep action hover distinct from default without hand-tuning forty state tokens.

The TUI chrome follows the shared ldnddev theme schema: ./dd_wcag_theme.yml, then ~/.config/ldnddev/dd_wcag_theme.yml, then built-in defaults (version: 1 required). F2 shows which source loaded. If your team already lives in the terminal, 0.5.0 fits the day you already have. For a longer take on why we keep building this way, our note on TUI tools and custom frameworks is still the through-line.

Get 0.5.0 from GitHub and try it

The repo is MIT licensed: github.com/ldnddev/dd_wcag. 0.5.0 ships an illustrated tutorial covering Contrast, Fix, Palette, and theming, plus an installer that will not overwrite a theme you already customized.

From a checkout:

./install.sh
dd_wcag

That builds a release binary into ~/.local/bin and copies a default theme to ~/.config/ldnddev/ only if one is missing. From a remote repo in one step:

./install.sh --repo https://github.com/ldnddev/dd_wcag.git --branch master

Or skip the install and run cargo run. You need a truecolor terminal. Uninstall is ./install.sh -uninstall.

Give it fifteen minutes on a real project, not the tutorial defaults. Paste the heading color and the page background you shipped last week. Set size and weight to the type styles in the component. Flip the APCA bar to Lc75. If it fails, open Fix before you rewrite the brand. Then drop the same three brand bases into Palette, generate, and see whether action text still passes on the hover surface. If export blocks, read the first failing group. That message is the ticket you would have gotten from QA in two sprints.

We built 0.5.0 because we were tired of accessible color being a late audit finding. Contrast is not a polish pass. It is a token decision. This release puts that decision in a tool you can run every day, then gives you a file your Framework theme can actually use. Pull it, try it, and tell us what still gets in your way.

Until next time, Jared Lyvers

Frequently Asked Questions

Does APCA replace WCAG 2.x for compliance?

No. WCAG 1.4.3 is still the bar most audits and contracts use. APCA is a better model of readability on modern screens, especially dark UI and small type. 0.5.0 shows both so you can pass the checklist and still judge whether people can actually read the text.

What does the palette builder export, and will it overwrite my theme?

After a generate, Ctrl+S writes a WCAG-gated _palette.scss (or Ctrl+C copies it). Names match the ldnddev Framework contract: $c_primary_default, action states, fixed text roles, support tokens. Export refuses blocking failures. The installer will not overwrite an existing dd_wcag_theme.yml.

How do I install dd_wcag 0.5.0?

Clone github.com/ldnddev/dd_wcag, then run ./install.sh to put a release binary in ~/.local/bin. Or run cargo run from the checkout. You need a truecolor terminal. The illustrated tutorial in the repo covers Contrast, Fix, Palette, and theming.