Accessible variant pickers and swatches on Shopify
Why color swatches and variant pickers fail shoppers, the pattern that works, how Dawn builds its picker, and a five-minute keyboard and screen reader test.
A variant picker is accessible when every option value is a real form control with a text name, the selected value is shown in more than one way, and sold-out values say so in words. Shopify’s Dawn theme gets most of this right: its swatches and pills are radio buttons with labels inside a named group. The trouble usually arrives with custom themes, swatch apps and design choices that lean on color alone. Here’s what goes wrong, what a good picker looks like in code, and how to test yours in five minutes.
What goes wrong with variant pickers and swatches?
The variant picker is the one control every shopper has to use before they can buy, so a problem here blocks the sale. The failures we see most, with the WCAG 2.2 criterion each one relates to:
- Color is the only clue. Swatches with no text, so “Sage” and “Olive” are two green squares. Shoppers with color blindness or low vision can’t tell them apart (1.4.1 Use of Color, Level A).
- The selected swatch is shown by color alone, or by a border one pixel thicker. Hard to see for anyone, invisible to some (1.4.1 again, and 1.4.11 Non-text Contrast, Level AA for the border itself, which needs 3:1 against what’s next to it).
- Sold out is a strike-through or a gray. No text says so, and a screen reader announces the value as if it were available (1.4.1, and 4.1.2 Name, Role, Value, Level A).
- Swatches with no name. An input whose label contains only a colored box, so a screen reader says “radio button, 1 of 4” and nothing else (4.1.2, and 3.3.2 Labels or Instructions, Level A).
- No group name. The values are labeled but nothing says which option they belong to, so “Small” could be a size or a bag (1.3.1 Info and Relationships, Level A).
- Built from generic elements. Some swatch apps and custom themes draw the picker with
divelements and click handlers. A keyboard can’t reach them, and a screen reader doesn’t know they’re choices (2.1.1 Keyboard, Level A). - No visible focus. The radio input is hidden and its label gets no focus style, so keyboard users can’t see where they are (2.4.7 Focus Visible, Level AA).
- Tiny targets. Swatches 16 pixels across, packed together, are hard to tap. WCAG asks for at least 24 by 24 CSS pixels, or enough space around each target (2.5.8 Target Size (Minimum), Level AA). Shopify’s own theme accessibility guidance goes further: primary touch targets, and it names variant selectors among them, “need to be at least 44 by 44 pixels.”
What does an accessible variant picker look like?
The pattern is old and boring, which is why it works: a group of radio buttons. Browsers and screen readers already know how to handle them, so you get keyboard support, a selected state and announcements for free. The job is to not break them while styling.
<fieldset class="variant-picker">
<legend>Color: <span data-selected-value>Sage</span></legend>
<input type="radio" id="color-sage" name="options[Color]" value="Sage" checked>
<label for="color-sage">
<span class="swatch" style="--swatch: #9caf88" aria-hidden="true"></span>
Sage
</label>
<input type="radio" id="color-olive" name="options[Color]" value="Olive">
<label for="color-olive">
<span class="swatch" style="--swatch: #6b7a3a" aria-hidden="true"></span>
Olive
</label>
<input type="radio" id="color-rust" name="options[Color]" value="Rust">
<label for="color-rust">
<span class="swatch" style="--swatch: #b7410e" aria-hidden="true"></span>
Rust <span class="variant-picker__status">(sold out)</span>
</label>
</fieldset>
What each part does:
- The
fieldsetandlegendgive the group a name. A screen reader announces “Color” before the first value, and the legend doubles as the visible “Color: Sage” line that sighted shoppers rely on. A few lines of script update the selected value when it changes. - Each
inputis a native radio button. Tab reaches the group, the arrow keys move between values and select them, and the browser tracks which one is checked. - Each
labelcarries the value’s name as text, so the swatch is never the only clue. The colored square is markedaria-hiddenbecause the text already says what it is. - “Sold out” is in the label, in words. The input stays enabled, so a shopper can still pick Rust and see which sizes exist in it.
The CSS hides the native radio circle without removing it from the page, then draws the selected and focused states in ways that don’t rely on color alone:
/* Hide the radio visually; keep it for keyboards and screen readers. */
.variant-picker input[type="radio"] {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.variant-picker label {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 44px;
padding: 8px 14px;
border: 2px solid transparent;
border-radius: 999px;
}
.variant-picker .swatch {
width: 24px;
height: 24px;
border: 1px solid rgb(0 0 0 / 0.5);
border-radius: 50%;
background: var(--swatch);
}
/* Selected: a solid border and a check mark, not only a color change. */
.variant-picker input:checked + label {
border-color: currentColor;
font-weight: 700;
}
.variant-picker input:checked + label::before {
content: "✓ ";
}
/* Keyboard focus: a ring with an offset, so the border never hides it. */
.variant-picker input:focus-visible + label {
outline: 3px solid #2453d6;
outline-offset: 3px;
}
Two details matter. Don’t use display: none on the input: that removes it from the keyboard order and from the screen reader. And give the focus ring its own offset so it stays visible on the selected pill, where the border is already solid. The thin border on each swatch keeps a pale color visible against a white background; check it with the contrast checker, because 1.4.11 asks for 3:1 between a control and what’s next to it. The check mark is drawn with CSS, which some screen readers read aloud; that’s harmless, because the radio’s checked state is what they announce as “selected”.
How does Dawn build its variant picker?
Checked against Dawn 16, Shopify’s open-source theme, the variant picker follows this pattern. In the theme editor (Online Store › Themes › Customize › open a product page › select the Variant picker block), the block has a setting for the picker type, a dropdown or pills, and a swatch shape. When an option’s values have a color or image attached to them in your admin and the swatch shape isn’t set to none, Dawn shows swatches instead of pills, or a swatch beside the dropdown. If no value has a swatch, you get pills or a dropdown.
For pills and swatches, Dawn wraps the option in a fieldset whose legend is the option’s name. For swatches, the legend also shows the selected value, so sighted shoppers see “Color: Sage” above the swatches, and it updates as they choose:
<fieldset class="js product-form__input product-form__input--swatch">
<legend class="form__label">
{{ option.name }}:
<span data-selected-value>{{ option.selected_value }}</span>
</legend>
...
</fieldset>
Each value is a radio input followed by a label. A pill’s label contains the value’s name as visible text. A swatch’s label contains the colored swatch plus a visually hidden span with the value’s name, so a screen reader hears “Sage” even though sighted shoppers see only the color. Both kinds of label also include a visually hidden note, “Variant sold out or unavailable”, for values that aren’t available in the current combination, so a screen reader hears it while sighted shoppers see the value styled as unavailable, crossed out in the case of pills:
<input
type="radio"
id="{{ input_id }}"
name="{{ input_name }}"
value="{{ escaped_value }}"
form="{{ product_form_id }}"
>
<label for="{{ input_id }}">
{{ value }}
<span class="visually-hidden label-unavailable">
{{ 'products.product.variant_sold_out_or_unavailable' | t }}
</span>
</label>
The dropdown type uses a label tied to a select, which is the most robust choice of all, at the cost of hiding the values until it’s opened.
For Shopify stores
Find and fix accessibility issues on your store
CodeInspire Accessibility Tool adds an accessibility widget for your shoppers, audits your pages against WCAG, and gives you step-by-step fixes for your Shopify admin, theme editor or theme code.
How do you test your variant picker in five minutes?
With a keyboard
- Open a product that has options. Press Tab until focus reaches the first option. You should see a focus ring on a swatch, pill or dropdown.
- Press the arrow keys. Focus and selection should move between values, the price and image should update, and the selected value’s name should appear somewhere in text.
- Tab onward to the quantity field and the add-to-cart button. Nothing should trap you, and nothing should need a mouse.
- Choose a sold-out value. Does the page say so in words, next to the value or on the button?
With a screen reader
VoiceOver is built into every Mac (Cmd+F5) and iPhone, and NVDA is a free screen reader for Windows. Move to the picker. You should hear the option’s name (“Color”), then each value with its name and state, for example “Sage, selected, radio button, 1 of 3”. A sold-out value should say so. If you hear “radio button” with no name, or nothing at all, the picker needs fixing.
With zoom and the contrast checker
Zoom the page to 200%. Swatches shouldn’t overlap, get cut off or lose their labels. Then check the selected border and any pale swatch against the background with the contrast checker.
With an automated check
The free website accessibility checker catches form controls with no label and low-contrast text. It can’t judge the rest, which is why the manual test comes first.
What if your theme or app fails the test?
- Try the theme editor first. Switch the Variant picker block to pills or a dropdown and test again. If a swatch app is involved, turn it off for one product and compare.
- Ask the app developer three questions: Are the swatches radio inputs with labels? Are the selected and sold-out states shown in text, not only by style? Can every swatch be reached and chosen with Tab and the arrow keys? A developer who tests with a keyboard will have answers.
- Add custom CSS for a weak focus ring or a selected state that’s too subtle. The styles above work in most themes with the class names changed.
- Get the code fixed if the picker is built from generic elements. Replacing it with the radio pattern is a theme code change; a developer can do it in an afternoon on most themes, and the Dawn source is a working reference.
What can’t a tool tell you?
- Whether “Sage” is a meaningful name. “Color 3” has a label and still tells a shopper nothing.
- Whether a swatch image shows the fabric well enough to choose by.
- Whether the selected state is obvious to a sighted shopper. A tool checks that markup exists, not that it’s clear.
- What happens for every combination. A tool checks the page as it loaded; sold-out handling shows up only when you change options.
- Anything in checkout. Your picker’s job ends at the add-to-cart button; Shopify runs checkout and publishes an accessibility conformance report for it.
What should you do next?
- Run the five-minute test on your best-selling product, on desktop and on a phone.
- Fix what you can in the theme editor today: picker type, swatch shape, color scheme contrast.
- Put what’s left on a list for your developer or app vendor, with the WCAG criterion next to each item.
- Run the free website accessibility checker on the same product page to catch the problems a tool can see, then work through its list of checks to do by hand.
Related guides
- Dawn theme accessibility: common issues and how to fix them
- Color contrast explained
- Shopify store accessibility checklist
Frequently asked questions
Are color swatches accessible?
They can be, if each swatch is a real form control with a text name, the selected swatch is shown by more than a color change, and sold-out values say so in text. A swatch that is only a colored square with a click handler fails shoppers who use a keyboard or a screen reader, and shoppers who can't tell the colors apart.
Should I use swatches, pills or a dropdown for variants?
All three can be accessible. A dropdown is the simplest and the hardest to get wrong. Pills show every value as text. Swatches show the color but need the value's name in text too, visible or at least for screen readers, and a selected state that doesn't rely on color alone. Pick the one your theme implements well, and test it.
How should a sold-out variant be shown?
In words, not only with a strike-through or a grayed-out swatch. Keep the value selectable so shoppers can see what's available in the other options, and say that it's sold out in text next to the value or on the add-to-cart button.
Why does my screen reader say 'radio button, 1 of 4' with no name?
The option values have no accessible name. Usually the swatch is an input with no label, or a label that only contains an icon or a colored box. Each input needs a label with the value's name, and the group needs a legend with the option's name, such as Color.
Sources
- Understanding Success Criterion 1.4.1: Use of Color
- Understanding Success Criterion 1.3.1: Info and Relationships
- Understanding Success Criterion 3.3.2: Labels or Instructions
- Understanding Success Criterion 4.1.2: Name, Role, Value
- Understanding Success Criterion 2.1.1: Keyboard
- Understanding Success Criterion 2.4.7: Focus Visible
- Understanding Success Criterion 1.4.11: Non-text Contrast
- Understanding Success Criterion 2.5.8: Target Size (Minimum)
- Shopify: Dawn theme source code (GitHub)
- Shopify: Accessibility best practices for themes
- Shopify Help Center: Accessibility in your online store
- Shopify Accessibility Conformance Report: Shopify Checkout