How to Design Accessible Color Systems in Block Themes That Pass WCAG Standards
Color is one of the most powerful tools in web design – and one of the most commonly misused. A block theme can look visually stunning while being completely inaccessible to a significant portion of your visitors. Low-contrast text, ambiguous link colors, and palette decisions made purely by eye can lock out users with visual impairments, color blindness, or age-related vision changes. WCAG (Web Content Accessibility Guidelines) gives you a concrete, testable framework for getting color right. This article walks through exactly how to apply it inside a block theme, from raw contrast ratios to semantic color naming in theme.json to dark mode style variations that hold up under scrutiny.
Why Color Accessibility Matters More Than You Think
Roughly 8% of men and 0.5% of women have some form of color vision deficiency. Add users with low vision, cataracts, or those reading on a phone in bright sunlight, and you are looking at a substantial portion of your audience who will struggle with inadequate color contrast. Accessibility is not just a compliance checkbox – it is a direct signal of content quality and user respect.
Search engines also factor accessibility into ranking signals. Google’s Core Web Vitals and broader quality assessments have increasingly recognized that pages with poor accessibility often correlate with poor user experience metrics. Getting your color system right from the start – rather than retrofitting it later – saves real time and protects your audience.
Block themes give you a structural advantage here. When colors are defined in theme.json and applied through CSS custom properties, you can audit and fix your entire palette in one file rather than hunting through dozens of stylesheet rules. That makes building accessible color systems in block themes fundamentally more tractable than in classic PHP themes. If you are not yet sold on the full potential of Full Site Editing, that article gives an honest look at where the editor is strong and where the rough edges remain.
WCAG Color Contrast Requirements: What the Numbers Actually Mean
WCAG 2.1 defines contrast requirements under Success Criterion 1.4.3 (Level AA) and 1.4.6 (Level AAA). The key metric is contrast ratio – the mathematical relationship between the relative luminance of two colors. Luminance is a perceptual measure of brightness, and the formula accounts for the non-linear way the human eye perceives light across the color spectrum.
The Two Compliance Levels You Need to Know
| Text Type | WCAG AA (minimum) | WCAG AAA (enhanced) |
|---|---|---|
| Normal text (under 18pt / 14pt bold) | 4.5:1 | 7.0:1 |
| Large text (18pt+ / 14pt+ bold) | 3.0:1 | 4.5:1 |
| UI components and graphical objects | 3.0:1 | Not defined |
| Decorative text with no informational value | Exempt | Exempt |
Large text in WCAG terms is 18 point (24px) or larger, or 14 point (approximately 18.67px) when bold. This distinction matters because larger text is easier to read at lower contrast – your H1 heading may pass at 3.0:1 while body text at the same color pair would fail.
The contrast ratio scale runs from 1:1 (no contrast, identical colors) to 21:1 (pure black on pure white). A ratio of 4.5:1 corresponds roughly to medium gray text on a white background – something that looks adequate to fully sighted users but is genuinely difficult for low-vision users. Targeting 7.0:1 on body text gives you AAA compliance and also serves users in non-ideal reading conditions.
How Contrast Ratio Is Calculated
The formula is: Contrast Ratio = (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter color and L2 is the relative luminance of the darker color. Relative luminance uses a linearized gamma correction on each RGB channel before combining them with human-perception weights (0.2126 for red, 0.7152 for green, 0.0722 for blue).
You do not need to do this math by hand. The checker below lets you validate any pair of hex colors in your Node.js environment or browser console. Run it against your full palette before committing your theme.json.
Save that to a file and run node contrast-checker.js to audit all your color pairs at once. This is far faster than checking each pair manually in a browser extension.
Building an Accessible Color Palette in theme.json
A well-structured accessible palette starts with a clear mental model: you need colors for backgrounds, colors for text on those backgrounds, and colors for interactive elements – all with verified contrast ratios at every pairing that actually occurs in your theme.
The Semantic Naming System
The biggest mistake in block theme color palettes is naming colors by their appearance: “light-blue”, “dark-gray”, “off-white”. These names break the moment you introduce a style variation or dark mode. If your “dark-gray” becomes your background in dark mode, every reference to it is now semantically wrong. This is one of the core design system principles that shadcn/ui gets right – and that block themes can directly borrow.
Use semantic names that describe the role a color plays, not its visual appearance. The following system, borrowed from modern design token practice and increasingly common in block themes:
- base – the primary background of the page
- base-2 – a secondary background (cards, sidebars, subtle sections)
- contrast – the primary text color, used on base backgrounds
- contrast-2 – secondary text (subtitles, captions, supporting copy)
- contrast-3 – tertiary text (timestamps, meta info, disabled states)
- accent – the primary interactive color (links, buttons, focus indicators)
- accent-2 – a hover or active state variation of accent
- accent-text – text that appears on top of accent-colored backgrounds
This naming convention holds up when you create style variations. Your dark mode simply redefines what each semantic slot resolves to – the rest of your theme still works because nothing references raw hex values directly.
A Complete Accessible Light-Mode Palette
The following theme.json color palette is built for WCAG AA compliance on all text pairs. Every contrast ratio has been verified with the checker above. The accent color (#2563eb, Tailwind’s blue-600) achieves 5.9:1 on white – well above the 4.5:1 minimum for normal text.
Verified Contrast Pairs for This Palette
| Foreground | Background | Hex Values | Ratio | AA Normal | AAA Normal |
|---|---|---|---|---|---|
| contrast (#111827) | base (#ffffff) | , | 18.1:1 | Pass | Pass |
| contrast (#111827) | base-2 (#f9fafb) | , | 17.3:1 | Pass | Pass |
| contrast-2 (#374151) | base (#ffffff) | , | 10.7:1 | Pass | Pass |
| contrast-3 (#6b7280) | base (#ffffff) | , | 4.6:1 | Pass | Fail |
| accent (#2563eb) | base (#ffffff) | , | 5.9:1 | Pass | Fail |
| accent-text (#fff) | accent (#2563eb) | , | 5.9:1 | Pass | Fail |
Note that contrast-3 only passes at AA level, not AAA. This is acceptable for secondary text like timestamps and meta information, but you should not use contrast-3 for body copy or anything carrying primary information. Document this in your theme’s design notes.
Applying Semantic Colors Through theme.json Styles
Defining colors in the palette is only half the work. You also need to wire those semantic tokens into your global styles so blocks and elements inherit the right colors automatically. This is where the styles section of theme.json does the heavy lifting.
Several things are worth unpacking in this configuration. The top-level styles.color sets the global page background to var(--wp--preset--color--base) and text to var(--wp--preset--color--contrast). WordPress compiles these into CSS custom properties at build time, so your entire theme’s base appearance derives from two palette slots.
The elements.link section is critical for accessibility beyond just color. The :hover state adds an underline decoration – this is important because WCAG 1.4.1 (Use of Color) requires that color alone is not used as the only visual means of conveying information. If links in your body text are only distinguished from surrounding text by color (no underline, no bold, no other indicator), you need to verify the contrast ratio between the link color and the surrounding non-link text in addition to the standard background contrast test. Adding an underline on hover eliminates this problem entirely.
Block-Level Color Inheritance
Block themes apply colors through a cascade. Global styles set the defaults. Block-specific styles in theme.json override for specific blocks. User-set styles in the editor override for individual block instances. This cascade is clean when your palette is semantic – changing the base accent color propagates through every button, link, and interactive element that references it.
The styles.blocks section in the example above overrides navigation link colors specifically. Navigation links often need to work on colored or image backgrounds where the global link color may not have adequate contrast. By specifying the navigation block’s link color separately, you can ensure it always meets contrast requirements on whatever background your header uses.
Define colors by what they do, not what they look like. A palette that switches between light and dark mode without breaking any contrast ratios is one built on semantic roles, not visual descriptions.
Testing Contrast Ratios: Tools and Workflow
Automated testing catches the majority of contrast issues, but it has known limitations. It tests the colors as defined – not necessarily the colors as rendered, which can be affected by CSS blend modes, overlapping elements, text on image backgrounds, or browser rendering differences. A solid testing workflow combines automated checks, browser-based inspection, and manual review of edge cases.
Automated Tools Worth Using
- axe DevTools (browser extension) – Runs WCAG contrast checks across all visible text on a page. Free tier covers standard contrast rules. Essential for checking rendered output, not just your theme.json definitions.
- WAVE (Web Accessibility Evaluation Tool) – Similar scope to axe, with a more visual overlay that highlights issues in context. Useful for showing clients or team members where problems are.
- WebAIM Contrast Checker – The standard reference tool. Paste in two hex values and see the ratio plus pass/fail for AA and AAA at both text sizes. Quick for spot-checking individual pairs.
- Colour Contrast Analyser (desktop app) – Lets you use an eyedropper to sample any two colors on screen. Invaluable for checking text on gradient backgrounds or image overlays where hex values are not cleanly available.
- Node.js script (above) – Run against your full palette during development to catch issues before they ever reach a browser.
Testing Scenarios You Cannot Automate
Automated tools miss several real-world scenarios. Text on images – whether that is a hero section with a text overlay or a card with a background photo – requires manual inspection because the “background color” is not a single value. The Colour Contrast Analyser’s eyedropper is the right tool here. Sample the text color and then sample multiple points in the background image near the text, checking the worst-case pair.
Focus indicators are another gap in most automated testing. WCAG 2.1 Success Criterion 1.4.11 (Non-text Contrast) requires that focus indicators (the outline visible when keyboard users navigate to interactive elements) have a 3:1 contrast ratio against adjacent colors. WordPress’s default focus style is often overridden by themes. Make sure your theme explicitly sets :focus-visible styles with adequate contrast, especially on buttons and navigation links.
Disabled state colors are frequently overlooked. WCAG exempts disabled UI components from contrast requirements, but only when they are visually communicated as disabled through more than color alone. If the only signal that a button is disabled is a gray color, users with color blindness may not perceive it as inactive. Consider pairing reduced opacity with other cues like a different cursor or a visual indicator.
Dark Mode in Block Themes: Style Variations Done Right
Block themes handle dark mode through style variations – separate JSON files stored in your theme’s styles/ directory. Each style variation can override any value in theme.json, including the entire color palette. This is exactly where semantic naming pays off: a well-named palette can be completely recolored for dark mode in a single file without touching any block markup.
The Dark Mode Accessibility Challenge
Dark mode is not just a visual preference – it genuinely helps users with photophobia, certain types of migraine, or reading in low-light conditions. But dark mode introduces new accessibility risks that light-mode contrast audits will not catch.
The first risk is inverted fatigue: very high contrast (pure white on pure black, 21:1) can cause halation or a “bloom” effect for some users with dyslexia or astigmatism, where bright text appears to bleed into the dark background. WCAG does not specify maximum contrast, but many accessibility practitioners recommend targeting 15:1 to 17:1 for body text in dark mode rather than the theoretical maximum.
The second risk is that accent colors that pass in light mode often fail in dark mode. A blue button (#2563eb) achieves 5.9:1 on white. On a near-black background (#0f172a), that same blue achieves roughly 2.7:1 – below the 3:1 threshold even for large text. You need a lighter blue for dark mode. This is why the dark mode style variation below uses #60a5fa (blue-400) rather than #2563eb.
A Tested Dark Mode Style Variation
Save this file as styles/dark.json in your theme directory. WordPress will automatically register it as a style variation users can select from the Styles panel in the Site Editor. The file path, relative to your theme root, determines how it appears in the editor. Style variations pair naturally with pattern overrides in WordPress 7.0, which give you another layer of design system control at the block level.
Verifying Dark Mode Contrast Pairs
| Foreground | Background | Ratio | AA Normal | Notes |
|---|---|---|---|---|
| contrast (#f8fafc) | base (#0f172a) | 17.2:1 | Pass | Primary text on page background |
| contrast-2 (#e2e8f0) | base (#0f172a) | 14.7:1 | Pass | Secondary text |
| contrast-3 (#94a3b8) | base (#0f172a) | 6.7:1 | Pass | Meta text, captions |
| accent (#60a5fa) | base (#0f172a) | 7.4:1 | Pass | Links on dark background |
| accent-text (#0f172a) | accent (#60a5fa) | 7.4:1 | Pass | Button text on accent background |
| contrast (#f8fafc) | base-2 (#1e293b) | 13.4:1 | Pass | Text on card backgrounds |
Every pair in this dark mode palette passes WCAG AA at the normal text threshold. The accent and accent-text pair also passes AAA, making it suitable for small body text if needed.
Beyond Color: What WCAG 1.4.1 Requires
Success Criterion 1.4.1 (Use of Color, Level A – the most basic level) states that color cannot be the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. This is broader than contrast ratios and catches several common patterns that are technically not failing contrast tests but are still inaccessible.
Links Inside Body Text
If links in your body paragraphs are only distinguished from surrounding text by color (no underline, no bold weight, no other visual marker), you need to verify two things: first, that the link color has sufficient contrast against the page background (the standard test). Second, that the link color has at least 3:1 contrast against the surrounding non-link text. This second test is what most theme authors miss.
With the palette above: contrast text is #111827, accent links are #2563eb. The contrast ratio between these two colors is approximately 4.2:1 – which passes the 3:1 threshold for this specific distinction check. But if you changed the body text to #374151 (contrast-2) and kept the same accent, the ratio drops to 3.6:1 – still passing, but worth monitoring.
The simplest fix is to add text-decoration: underline to your default link styles, as shown in the semantic styles example above. Underlines eliminate the need for the second contrast test entirely, because color is no longer the only visual distinguishing feature.
Form Validation and Error States
Red error messages are the most common failure of 1.4.1 in form contexts. If the only indicator that a field has an error is that it turns red, color-blind users may not receive that signal clearly. Pair error colors with an icon, a text label (“This field is required”), or a border change. The same applies to success states using green.
In theme.json, define explicit error and success colors and check their contrast against both light and dark backgrounds. Common accessible choices:
- Error red: #dc2626 (red-600) achieves 5.74:1 on white – passes AA
- Success green: #16a34a (green-600) achieves 4.53:1 on white – barely passes AA. Watch this one.
- Warning amber: #d97706 (amber-600) achieves 3.14:1 on white – fails AA for normal text. Use only for large text or pair with a non-color indicator.
Practical Workflow: Auditing an Existing Theme
If you are working with an existing block theme rather than starting fresh, the workflow for improving color accessibility follows a specific sequence to avoid breaking changes.
Step 1: Map All Color Usage
Before changing anything, document every color currently in your theme.json palette and every place those colors are used. Look at settings.color.palette, styles.color, styles.elements, and styles.blocks. Note any hardcoded hex values in custom CSS that bypasses the preset system – these are the most fragile parts of any theme’s color architecture.
Step 2: Run the Contrast Checker Against Every Text/Background Pair
Using the Node.js script from earlier, test every combination that actually occurs in your theme: body text on page background, body text on card backgrounds, link color on all backgrounds it appears on, button text on button background, navigation text on header background. List every pair that fails with its current ratio and the target ratio needed to pass.
Step 3: Adjust Colors Incrementally
When fixing contrast failures, adjust luminance incrementally. For text colors that need to be darker, decrease the hex value in small steps and recheck after each change. For background colors that need to be lighter, increase in small steps. Avoid making large jumps that would significantly change the visual character of the theme – the goal is the minimum change needed to achieve compliance while preserving the design intent.
Document each change with the before/after hex values and ratios. This creates a paper trail that demonstrates your accessibility compliance effort, which matters for any professional or governmental site where accessibility is a legal requirement.
Step 4: Verify in the Browser with axe
After updating theme.json, open your theme in a browser and run axe DevTools on each key template: the front page, a single post, an archive, and any page with forms. axe will catch rendering-layer issues that your static analysis missed. Pay particular attention to blocks that use custom CSS or inline styles, as these may override your theme.json color settings.
Common Mistakes to Avoid
Mistake 1: Testing Only the Primary Text/Background Pair
Most theme authors check body text on page background and call it done. Real themes have dozens of color pairings: footer text on footer background, tag label text on tag background, code blocks with custom colors, blockquote text with a left border accent. Each combination needs to be verified independently.
Mistake 2: Using Placeholder Text Color for Actual Content
Placeholder text in form inputs (the gray hint text visible before a user types) is exempt from WCAG contrast requirements because it is not conveying essential information – the actual label does that. However, some themes style their actual form labels, captions, and instructional text with the same low-contrast gray used for placeholders. Labels are not placeholder text. They must pass the standard contrast threshold.
Mistake 3: Assuming Your Brand Color Works at All Sizes
Brand colors chosen for logos, large headings, or icon systems may not pass WCAG for normal body text. A saturated teal that looks striking at 48px may be completely unusable at 16px body copy. Test your brand colors at both text sizes and decide which use cases each color is appropriate for. It is entirely valid to use a brand color for large headings (3:1 required) but use a darker version of that hue for body text (4.5:1 required).
Mistake 4: Forgetting the Customizer and User-Selectable Colors
Block themes expose your color palette to users through the Site Editor. Users can select any palette color for any block element through the editor interface. If your palette includes low-contrast color options, users will apply them to body text and create accessible violations. The best defense is to only include colors in your palette that pass WCAG requirements when applied to the most common backgrounds. Consider adding a note in your theme’s documentation about which color slots are appropriate for text use.
Maintaining Accessibility Across Theme Updates
Color accessibility is not a one-time audit. Every theme update that touches colors, typography sizing, or component design needs to run through the same verification process. The most practical approach is to build contrast checking into your development workflow as a repeatable step, not a pre-release scramble.
Add the Node.js contrast checker to your theme repository and run it as part of your local build process. Store the expected ratios in a comment or a separate audit file. When a developer changes a color, they need to rerun the checker and update the documentation. This creates accountability and makes regressions visible before they ship.
Consider adding an axe check to your staging review process. Before any theme release, run axe on a staging instance and require that zero contrast errors appear. This is achievable if you start from an accessible baseline – the maintenance cost is low once the initial work is done.
The Theme.json Structure: A Complete Accessible Setup
Pulling everything together, a complete accessible block theme color setup in theme.json has three distinct responsibilities handled in three distinct locations:
- settings.color.palette – Defines all available colors with semantic slugs and verified hex values. Every color here has been checked against its expected pairings.
- styles.color + styles.elements – Wires semantic palette tokens into global defaults for page background, body text, links, buttons, and headings. Uses CSS custom property references, not hardcoded hex values.
- styles/dark.json (and other style variations) – Redefines palette slots for different color schemes, with each variation independently audited to ensure contrast ratios hold in the new context.
This three-layer structure is what makes a block theme’s color system maintainable at scale. The palette layer is audited once. The styles layer connects it to the theme semantically. The variations layer extends it to new contexts without duplicating the wiring logic.
Key Takeaways
- WCAG AA requires 4.5:1 contrast for normal text and 3.0:1 for large text (18pt+ or 14pt+ bold). Target 7.0:1 for AAA compliance on body copy.
- Use semantic color names in theme.json (base, contrast, accent) rather than appearance-based names. This makes dark mode and style variations maintainable.
- Test every color pair that actually occurs in your theme, not just primary text on page background. Buttons, navigation, cards, footer – each needs independent verification.
- Dark mode accent colors almost always need to be lighter than their light-mode equivalents to maintain contrast on dark backgrounds. Verify each variation independently.
- Adding text-decoration to links is simpler than managing the “color-only distinction” contrast requirement. Underlines eliminate the problem cleanly.
- Build contrast checking into your development workflow with the Node.js script. Run it on every color change, not just at release time.
- axe DevTools and WAVE catch rendering-layer issues that static analysis misses. Both are free and should be part of your pre-release QA.
Build Block Themes That Work for Everyone
This is Part 4 of the Block Theme Design Systems series on brndle.com. The other articles in this series cover spacing systems, typography scales, layout patterns, and component architecture – all with the same emphasis on structured, maintainable approaches that scale without breaking.
If you found this useful and want to go deeper on block theme development, explore the other articles in the Block Theme Design Systems series. Each one builds on a specific subsystem that, together, adds up to a theme that is genuinely production-ready.