Fix
Text that fails colour contrast
Contrast is the failure customers describe as "I couldn't read it". The maths is settled; the colour is a brand decision, so a person on your side approves every change.
Who this locks out
Low-contrast text is unreadable for many people — and a brand decision, so a human always approves the fix.
- Shoppers with low vision or early cataracts
- Anyone reading a phone in daylight glare
- Customers with reduced colour vision
- Older customers on ageing, uncalibrated screens
The transform
Before
<a class="button add_to_cart_button" style="background:#f4f4f4;color:#8a8a8a">Add to basket</a> After
<a class="button add_to_cart_button" style="background:#f4f4f4;color:#595959">Add to basket</a> Detection
How this is tested here
Every scan runs three independent engines and nine custom probes against your rendered pages, and records which engine flagged what. These are the real rules behind this fix family.
| Criterion | Level | Detected by | What we do |
|---|---|---|---|
| 1.4.3 Contrast (Minimum) | AA | axe-core (color-contrast) · IBM Equal Access (text_contrast_sufficient) | review |
This family is keyed to the axe rules color-contrast in the
engine's own category map.
What Klarvo Access does
Klarvo measures the failing pair, generates a corrected colour that holds your hue and clears the required ratio, and queues it for a person on your side to approve — a contrast change is never applied silently.
Review queue
Generated with a proposed value, then queued for you to approve, edit or reject. It is never applied silently, because this one is a judgement call.
An automated finding is evidence, not a certificate. Conformance is certified only by the professional audit — how that works.
Do it yourself
Fixing this by hand
What low contrast does to a shop
Picture the pale grey sale price under a product thumbnail. The hint text inside the search box. The delivery-charge line under the basket subtotal, set two shades lighter than the total so it would recede. The red validation message at checkout, legible on a calibrated studio monitor and invisible on a five-year-old laptop in a bright kitchen.
Each of those is a person losing information they need in order to buy.
A customer in her sixties with early cataracts reads your product titles without difficulty and misses the line that says delivery is charged separately. A shopper with reduced colour vision cannot tell the discounted price from the struck-through original when both are rendered grey. Someone on a platform in July, phone at full brightness, cannot find the coupon field’s label at all. None of them complain. They abandon the basket, and your analytics record an ordinary exit.
Checkout costs the most, because that is where text is smallest, densest and most legally load-bearing: prices, delivery terms, field labels, error messages. WCAG’s success criterion 1.4.3 Contrast (Minimum), a Level AA requirement, sets the floor: a ratio of at least 4.5:1 between text and its background, relaxed to 3:1 for large-scale text — at least 18 point, or 14 point bold.
How it is detected here
Two independent engines measure this criterion, and Klarvo runs both against the rendered page rather than reading your stylesheets:
- axe-core 4.12, rule
color-contrast - IBM Equal Access, rule
text_contrast_sufficient
HTML_CodeSniffer, the third engine, also reports against 1.4.3 without exposing a separately named rule id. Where two engines with different implementations agree an element fails, that is the strongest signal available before a human looks; where they disagree, the item goes to a person rather than into a fix.
There is no bespoke Klarvo probe for 1.4.3, and none is needed: the ratio is arithmetic on the colours the browser actually computed, and both engines already do that arithmetic correctly. Klarvo reserves its own probes for ground the engines cover thinly or not at all — reflow, keyboard traps, visible focus — and contrast is not one of those.
The honest limits matter as much as the findings. The axe rule will not report on text that sits on a background-image, text obscured by another element, or images of text; and it returns incomplete rather than a verdict where a gradient, a pseudo-element or an ancestor’s positioning makes the background colour indeterminate. Those become review items carrying a screenshot, never silent passes. An automated finding is evidence, not a certificate.
Contrast of non-text things — icon-only controls, input borders, focus rings — is a different success criterion (1.4.11 Non-text Contrast). No engine in this stack has a named rule for it, so it belongs to human audit, not to this page.
What Klarvo does about it
Colour contrast sits in the review tier. The engine generates the fix; a person on your side approves it; nothing is applied silently.
That is deliberate. The ratio is arithmetic, but which of the two colours moves is a decision about your shop front: software should not quietly darken your accent pink across every product card because a calculation said it could.
The review item shows the page and element, the measured ratio, the two colours, and a proposed replacement that holds hue and saturation and adjusts lightness only until the pair clears 4.5:1 — or 3:1 where the text is genuinely large-scale. You accept it, edit the hex, or fix it in your theme instead.
Once approved, the change is applied server-side in the HTML your store serves — a scoped declaration on the affected selector, never a client-side overlay — and then re-validated by re-running the same two engines against the served page. If the re-check is not clean, the item returns to the queue instead of being marked done.
The applied fix keeps the store readable while the durable work — in your own theme — is scheduled.
How to fix it yourself
Measure first. Inspect the element in Chrome, Edge or Firefox and open the colour swatch beside its color value: the picker prints the contrast ratio and the AA threshold it must cross. Do it before changing anything, so you know which of the two colours is wrong.
Then fix the token, not the instance. In a block theme, go to Appearance → Editor → Styles → Colors and correct the palette entries and the Elements colours (Text, Links, Headings, Buttons). To make it survive updates, put the same values in a child theme’s theme.json:
{
"version": 3,
"settings": {
"color": {
"palette": [
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#1c1c1c", "name": "Contrast" },
{ "slug": "muted", "color": "#595959", "name": "Muted text" }
]
}
},
"styles": {
"color": { "text": "var(--wp--preset--color--contrast)" },
"elements": {
"button": { "color": { "background": "#1f3a5f", "text": "#ffffff" } }
}
}
}
In a classic theme, keep the overrides in a child theme stylesheet and enqueue it after Woo’s own CSS so it wins on order rather than on !important:
// wp-content/themes/your-child/functions.php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'child-contrast',
get_stylesheet_directory_uri() . '/style.css',
array( 'woocommerce-general' ),
wp_get_theme()->get( 'Version' )
);
} );
The selectors that actually fail in a WooCommerce store. These are the recurring offenders across the shop archive, the product page and checkout:
/* Prices in the grid and on the product page */
.woocommerce ul.products li.product .price,
.woocommerce div.product p.price { color: #1c1c1c; }
/* The struck-through original price: faded, not decorative */
.woocommerce ul.products li.product .price del,
.woocommerce div.product p.price del { color: #595959; opacity: 1; }
/* Store notices, shipping calculator, coupon row */
.woocommerce-info,
.woocommerce-message { color: #1c1c1c; background-color: #eef2f7; }
/* Hint text and unavailable controls */
.woocommerce form .form-row input::placeholder { color: #595959; }
.woocommerce button.button:disabled { color: #4a4a4a; background-color: #e2e2e2; }
/* Required marker and validation text */
.woocommerce form .form-row .required,
.wc-block-components-validation-error { color: #a3161b; }
If your store uses Woo’s Cart and Checkout blocks rather than the classic shortcodes, those selectors will not reach them. The blocks style through their own .wc-block-components-* classes and custom properties, so set global values in Styles or theme.json and target the block classes directly.
Four things that quietly undo the work. First, opacity on text: the engines measure the colour that results after blending, so a faded near-black usually still fails — use a genuinely lighter hex instead. Second, text over a hero or category banner image: there is no fixed background to measure, so place the text on a solid or near-solid panel, for example background: rgba(15, 20, 28, 0.72), or every scan will hand it back to a human. Third, states: a:hover, :focus-visible and :visited frequently lighten a link that passes at rest. Fourth, WCAG treats text inside an inactive control as incidental, so a disabled button is not strictly required to meet the ratio — but a greyed-out “Add to basket” is often the only signal that a variation is unavailable, so make it readable anyway.
One more: placeholder text is not a label. If it carries the only description of the field, correcting its colour still leaves the field unlabelled — see form labels.
How you know it worked
Re-scan the same URLs and expect color-contrast and text_contrast_sufficient to come back clean on the named elements. Keep the before-and-after report: a dated finding on a specific page is the evidence an auditor and a regulator both ask to see.
Then check by hand, because a clean scan of a resting page proves less than it looks. Walk the states rather than the pages, confirming each in the devtools colour picker: hover a price, focus the coupon field, submit checkout empty so the validation message renders, open an out-of-stock variation, and view a product on sale so both prices appear at once.
Cover the pages that carry the shopper to payment — shop archive, single product, basket, checkout, account login — and any banner with text over an image, which the engines cannot settle for you.
Finally, put a recurring scan behind it. Conformance is judged as of a date and for a defined scope, and colour tokens travel with theme, plugin and page-builder releases. An item you cleared last month can reappear from a stylesheet you never touched — which makes re-checking routine maintenance, not an admission of anything.
Sources: W3C WAI — Understanding Success Criterion 1.4.3: Contrast (Minimum) · W3C — Technique G18: ensuring a contrast ratio of at least 4.5:1 · W3C — Technique G145: ensuring a contrast ratio of at least 3:1 · Deque University — axe-core 4.12 rule: color-contrast · WordPress Documentation — Styles overview (colours, palettes, custom CSS)
FAQ
Questions this raises
Will this change my brand colours?
Only the pairs that fail, only where they sit behind text, and only after you approve them. The proposed colour holds your hue and saturation and moves lightness until the pair clears the ratio, so a navy stays navy. You can also edit the hex or reject it and fix it in your theme instead.
I darkened the text once. Why did it come back?
Colour tokens ship with releases. A theme update, a Woo update or a new payment plugin can reintroduce a pale grey you had already corrected, and states you never see in a screenshot — hover, focus, disabled, on-sale — carry their own colours. Contrast is a thing you re-check, not a thing you finish.
Why does the scan say a banner needs review instead of just fixing it?
Text sitting on a photograph or a gradient has no single background colour to measure, so the ratio is genuinely indeterminate. The engines return that as incomplete rather than pass or fail, and it goes to a person with a screenshot. Reporting the uncertainty honestly is the point.
Find out whether your store has this one.
The free scan checks your real pages — home, product, populated basket, hydrated checkout — with all three engines and all nine probes, and tells you exactly what it found and where.