Every DTC brand I have worked with that tried to pass WCAG AA with a full brand palette hit the same tension. The brand pink is 4.1:1 against ink. AA requires 4.5:1 for body text. The accessibility audit flags it, the team gets nervous, and someone suggests a "safer palette" which turns out to be beige and cream. Six weeks later the brand looks like every other wellness startup. The visual identity is gone. The audit passes.
This is the wrong trade. The fix is not to weaken the brand color. It is to scope where each color is allowed to carry body text, and to treat accessibility as a role constraint, not a palette constraint.
- Signal4.1:1 fail
- Accent 22.3:1 fail
- Warm3.8:1 fail
- Cool8.2:1 pass
The contrast math
WCAG AA requires 4.5:1 contrast for body text and 3:1 for large text (18pt or 14pt bold, which works out to roughly 24px or 18px bold on most screens). AAA is 7:1 for body and 4.5:1 for large.
A saturated brand color like #fd109a (this site's pink) has about 4.1:1 against black. It fails AA for body. It passes AA for large text. That is a useful split: the brand color can carry headlines but cannot carry paragraphs.
The mistake is treating this as a palette-wide constraint ("our pink fails, so pink is out"). The correct read is role-scoped ("pink can be used for h1 and display but cannot be used for body copy"). The brand gets to keep its identity on headings; body copy gets rendered in ink on paper and passes AA trivially.
The role layer
In the token system, this translates to naming tokens by what they can do, not by which colors they are. A small set of semantic color roles that encode accessibility into the name:
/* Brand identity, restricted to accent roles */
--signal: #fd109a; /* Headings, buttons, links */
--signal-ink: #0a0a0a; /* Text on signal background */
/* Body palette, always AA-compliant */
--ink: #0a0a0a; /* Body text, most UI */
--ink-dim: #4a4a4a; /* Secondary text, still AA against paper */
--paper: #fafafa; /* Default background */
--paper-warm: #f4ede0; /* Optional warm background variant */
A component that renders body text uses --ink or --ink-dim. A component that renders a headline uses --signal. The designer does not have to remember the contrast ratio; the token name carries the constraint. A body component with color: var(--signal) would be an obvious role violation, visible in review.
The background rule
Body text usually lives on a light background (paper or a warm variant). The accessibility check is against whatever background color the text is rendered on. The rule I enforce:
- Body text renders on
--paperor--paper-warmonly. - Body text color is
--ink(for primary) or--ink-dim(for secondary). - Never render body text on
--signalbackground. That combination always fails either AA or readability or both. - If a section needs a colored background, it contains only large text (headlines, button labels) or image content.
This constraint sounds restrictive. It is, for the good reason that reversing body text on a brand-color background is the most common contrast failure in DTC design. The "this section is on pink" impulse produces illegible copy. Enforcing the rule at the token layer prevents the mistake by default.
The button problem
Buttons are the interesting case because they often combine brand color with text. A --signal background with white text works at 4.7:1 (#fd109a against #fafafa). A --signal background with --ink text (nearly black) works at 4.1:1, which fails AA for the smallest button sizes.
The fix is to establish the minimum button height (which makes the text "large" by WCAG rules) or to use --signal-ink which is a color specifically chosen to pass against --signal. On this site, --signal-ink is --paper (nearly white), giving a 4.7:1 ratio. Buttons render --signal on the background, --signal-ink on the label.
The rule captures this: button color pairings are pre-approved in the token layer. The designer does not pick a text color for a button; they use the button component, which is configured with the correct pair.
The focus state exception
Focus indicators (the ring around a tab-navigated element) have their own contrast requirement: 3:1 against the background, per WCAG 2.2 Focus Appearance. This is often where brand-consistency hits reality. A thin pink focus ring against a cream background might be 2.8:1 (fail). A thicker ring, or a contrasting outer stroke, solves it without abandoning the color.
My default focus pattern: a 3px ring in --signal with a 1px offset in --ink. The offset provides the contrast against any background (including signal itself). Works across light, dark, and accent surfaces without a per-surface exception.
The illustration and image rule
Brand illustrations and product photography are an exception to the body-text rule. A product photo with a saturated background does not have to pass contrast because the "content" is the image, not overlay text. If overlay text is added (the campaign hero over a product shot), the text needs its own contrast check: either a darkened scrim behind the text, or a high-contrast text color that works against whatever hue dominates the underlying photo.
I use a standard "legibility overlay" in hero sections: a 20-40 percent darkening gradient under the text, which brings ink text to AA against any underlying image. The gradient is defined once in the token layer and referenced from any section that needs it.
“The accessibility fight is not between brand and compliance. It is between an undisciplined palette that puts body text everywhere and a scoped palette that puts body text only where it works.
”
The audit workflow
Two checks I run on every DTC brand build.
The first is automated: a Playwright script that navigates every key page (home, collection, PDP, cart, checkout, email-capture landing) and runs axe-core. Any contrast violation gets flagged with the exact token combination that failed. The fix is always at the token level, not the component level: rewrite the component to use a role-appropriate token, not adjust the color in place.
The second is visual: a PDF of the entire palette with every token pair (signal-on-paper, ink-on-paper, ink-dim-on-paper, signal-ink-on-signal) labeled with its contrast ratio. This PDF is part of the brand deliverable. A new designer or vendor reads it and knows which pairings are legal before they start.
Both checks take an hour to set up and run in a minute. The prevention value is dramatic: the contrast bugs that used to surface in pre-launch QA now surface at token-layer design time, which is where they are cheap to fix.
What to do if your brand color fails AA for headlines
This happens occasionally: a pastel or lower-saturation brand color fails AA even for large text (below 3:1). At that point the palette is fighting the brand itself; headlines in the brand color are unreadable without adjustment.
Two moves. First, try a slightly darker variant of the brand color for text use only ("--signal for the swatch, --signal-text for when it carries copy"). A 10-15 percent darker version usually pushes the contrast over 3:1 without looking like a different color to untrained eyes.
Second, accept that the brand color is for graphics and illustration, not for text. Use --ink for all headings and --signal as accent shapes, underlines, and illustration fills. The brand still feels present through placement and proportion; the text rendering stays compliant without compromise.
Integration with the design token system
This whole argument lives inside the broader design tokens that survive a rebrand pattern. Role-named color tokens are semantic tokens by another name. The accessibility constraint is one of the things the role layer encodes. When a future rebrand swaps the brand color, the role constraints survive: --signal may change value, but --signal-ink adjusts alongside it (usually auto-derived, sometimes re-picked), and every component downstream is still wearing a compliant pair.
A team that names colors by appearance (--pink-500, --cream-100) has to re-audit accessibility on every rebrand because the role information lives in the designer's head. A team that names by role (--signal, --ink, --paper) retains the compliance semantics across rebrands by design.
Frequently asked questions
Do I have to meet AA or AAA?
AA is the legal floor in most jurisdictions and the practical target for DTC ecommerce. AAA is stricter and usually unrealistic for a brand with a saturated accent color because 7:1 is hard to hit with anything other than very dark text on very light backgrounds. Aim for AA on everything, AAA on critical error messages and legal copy where you want stronger assurance.
What if my brand color is below 3:1 even at large text?
Use a slightly darker variant for text use (--signal-text separate from --signal) or accept that the brand color is for graphic elements only, with ink carrying the text. This is the right call for many pastel or wellness brands; the color still carries brand recognition through illustration and placement.
How do I handle dark mode?
Dark mode flips the paper/ink roles: --ink becomes the light color, --paper becomes the dark one. The signal usually needs a slight adjustment to stay AA against the new paper. The role-scoped token system handles this cleanly: you define a dark-mode override at the root level, and every component downstream adjusts automatically.
Is there a tool that checks this automatically?
axe-core (via browser extension or Playwright) catches contrast failures on live pages. Figma plugins like Stark and Contrast catch them at design time. I run axe-core on every PR via a Playwright job; Stark runs on the Figma files. Both are cheap and catch different failure modes (code-level vs. design-level).
What about hover states and disabled states?
Hover states typically darken the primary color, which improves contrast by 10-20 percent. Disabled states dim the color, which reduces contrast below AA; this is acceptable because disabled elements do not carry information required for task completion. Both patterns are standard and do not require special accessibility handling.
Sources and specifics
- Contrast ratios calculated against
#fafafapaper and#0a0a0aink. Specific values vary by brand color; the pattern (role-scoped tokens) is the transferable part. - WCAG 2.2 Success Criterion 1.4.3 (contrast minimum) requires 4.5:1 for body and 3:1 for large text; Success Criterion 2.4.11 (Focus Appearance) requires 3:1 for focus indicators. Both are part of every AA audit.
- The PDF palette deliverable pattern (labeled contrast pairs) is from the brand asset hub engagement documented at the brand asset hub case.
- axe-core integration in a Playwright job is the standard accessibility check on every Next.js and Shopify build I ship, including this site.
- See also: the brand architecture hub, design tokens that survive a rebrand, and the typography scale article.
- Full methodology is part of the Operator's Stack.
