Rubik Variant Images & Swatch includes built-in accessibility support targeting WCAG 2.1 Level AA compliance. All swatch types - image swatches, pill/text swatches, and dropdowns - work with keyboards, screen readers, and respect user motion preferences out of the box.
No configuration is needed. These features are enabled automatically for every store.
Customers who navigate with a keyboard can fully interact with all swatch types:
This follows the standard HTML radio group keyboard pattern, so it behaves the same way as native browser radio buttons.
When a customer navigates with the keyboard, a visible blue outline appears around the focused swatch.
This outline only appears during keyboard navigation - it does not show on mouse clicks, thanks to the :focus-visible pseudo-class.
Focus indicators work on both image swatches and pill/text swatches. The default style is a 2px solid #005fcc outline with 2px offset.
You can change the focus indicator appearance with CSS custom properties:
| Variable | Default | Description |
|---|---|---|
--rubik-swatch-focus-outline |
2px solid #005fcc |
Outline style for focused swatches |
--rubik-swatch-focus-outline-offset |
2px |
Space between the swatch border and the focus ring |
Example: match your brand color and increase the offset:
:root, :host {
--rubik-swatch-focus-outline: 2px solid #e63946;
--rubik-swatch-focus-outline-offset: 3px;
}
Dropdowns have their own focus styles, which are documented in the Custom CSS reference under the "Dropdown - focus" section.
When tooltips are enabled for image swatches (via the "Show tooltip" setting), they appear on both mouse hover and keyboard focus. When a customer tabs into a swatch, the tooltip shows the option value name above the swatch, just like it does on hover.
This uses the CSS :focus-within pseudo-class on the swatch wrapper, so no JavaScript is involved.
Every swatch includes ARIA attributes that give screen reader users clear context about each option.
Each radio input has an aria-label that combines the option name and value. For example:
This means screen reader users hear the full context of each option, not just the value. The "(unavailable)" suffix is appended automatically for sold-out or unavailable options.
When a customer selects a new option, a live region announces the change. For example, selecting a blue color swatch causes the screen reader to announce "Color: Blue selected".
This uses an aria-live="polite" region with role="status",
so the announcement waits until the screen reader finishes its current speech before reading the update.
The diagonal line SVG overlay on unavailable swatches is marked with aria-hidden="true".
Screen readers skip this visual indicator entirely - the unavailability state is already communicated through the aria-label text.
The swatch component uses proper semantic HTML that screen readers understand natively:
<fieldset> groups each option (Color, Size, etc.)<legend> provides the group label<input type="radio"> with shared name attributes creates proper radio groupsWhen a customer changes a swatch option using the keyboard, the app fetches updated availability data and replaces the swatch HTML. This DOM replacement would normally destroy the focused element, stranding keyboard users.
The app automatically saves which swatch had focus before the update, then restores focus to the same option after the new HTML is in place. This means keyboard users can smoothly navigate between options using arrow keys without losing their place.
The app respects the prefers-reduced-motion operating system setting. When a customer has enabled
"Reduce motion" (macOS) or "Show animations" off (Windows), all swatch transitions are effectively disabled:
This uses a 0.01ms transition duration (rather than 0s) to ensure any JavaScript that
listens for transitionend events still works correctly.
This section documents the accessibility implementation at a technical level, for developers integrating with or extending the swatch component.
Each <input type="radio"> swatch element has:
<input
type="radio"
aria-label="Color: Forrest Green"
class="rubik-swatch__input"
name="rubik-option-1"
value="Forrest Green"
data-rubik-option-position="1"
data-rubik-option-name="Color"
...
/>
For unavailable options, the aria-label value becomes "Color: Forrest Green (unavailable)".
This is generated server-side by the Liquid template using {% unless option_value.available %} (unavailable){% endunless %}.
A visually hidden <div> is the first child of the .rubik-swatch container:
<div
class="rubik-swatch__sr-announcement"
aria-live="polite"
aria-atomic="true"
role="status"
style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;
overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;"
></div>
The visually-hidden styles are inlined (not in the external CSS) so the element is hidden immediately, even before the stylesheet loads.
When a swatch is selected, JavaScript sets textContent on this element to trigger the screen reader announcement.
<svg class="rubik-swatch__unavailable-overlay" aria-hidden="true" ...>
<line x1="100" y1="0" x2="0" y2="100" stroke="currentColor" .../>
</svg>
The aria-hidden="true" attribute prevents screen readers from announcing the decorative strike-through line.
Focus indicators use the :focus-visible pseudo-class on the visually-hidden radio input, combined with the adjacent sibling selector (+) to style the visible label:
/* Keyboard focus on pill swatches */
.rubik-swatch__input:focus-visible + .rubik-swatch__label--pill {
outline: var(--rubik-swatch-focus-outline, 2px solid #005fcc);
outline-offset: var(--rubik-swatch-focus-outline-offset, 2px);
}
/* Keyboard focus on image swatches */
.rubik-swatch__input:focus-visible + .rubik-swatch__label--image {
outline: var(--rubik-swatch-focus-outline, 2px solid #005fcc);
outline-offset: var(--rubik-swatch-focus-outline-offset, 2px);
}
/* Show tooltip on keyboard focus */
.rubik-swatch__item-wrapper:focus-within .rubik-swatch__tooltip {
opacity: 1 !important;
visibility: visible !important;
}
The :focus-visible pseudo-class is key - it ensures the outline only appears during keyboard navigation, not on mouse or touch input.
This is supported in all modern browsers.
@media (prefers-reduced-motion: reduce) {
.rubik-swatch__label,
.rubik-swatch__image,
.rubik-swatch__tooltip,
.rubik-swatch__dropdown {
transition-duration: 0.01ms !important;
}
}
When a variant change triggers an HTML refresh (the app fetches new server-rendered HTML to update availability states), the focused element is destroyed and recreated. The app handles this in two steps:
Before DOM replacement - capture which swatch has focus:
const shadowActiveEl = webComponent.shadowRoot.activeElement;
let focusedOptionPosition = null;
let focusedOptionValue = null;
if (shadowActiveEl && shadowActiveEl.matches('.rubik-swatch__input')) {
focusedOptionPosition = shadowActiveEl.dataset.rubikOptionPosition;
focusedOptionValue = shadowActiveEl.value;
}
After DOM replacement - restore focus to the matching element:
if (focusedOptionPosition && focusedOptionValue) {
const escapedValue = focusedOptionValue
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"');
const restoredInput = rubikSwatch.querySelector(
`input[data-rubik-option-position="${focusedOptionPosition}"][value="${escapedValue}"]`
);
if (restoredInput) restoredInput.focus();
}
Focus is restored after autoSelectAvailableOptions runs, which ensures that if the app auto-switches an unavailable option,
the focus target is still valid.
The value is escaped for safe use in CSS attribute selectors (backslashes and double quotes are handled).
The swatch component renders inside a Shadow DOM (<rubik-swatch> custom element with mode: 'open').
This has several accessibility implications:
aria-live region works inside Shadow DOM in all modern browsers.:focus-visible and :focus-within work correctly within the shadow boundary.webComponent.shadowRoot.activeElement, which returns the focused element
within the shadow tree (or null if no shadow element has focus).
--rubik-swatch-focus-outline CSS variables via the app's custom CSS field instead.
| WCAG criterion | Level | How it's met |
|---|---|---|
| 1.3.1 Info and Relationships | A | Semantic HTML: <fieldset>, <legend>, <input type="radio"> with aria-label |
| 1.4.11 Non-text Contrast | AA | Default focus color (#005fcc) meets 3:1 contrast on light backgrounds; customizable via --rubik-swatch-focus-outline for dark themes |
| 2.1.1 Keyboard | A | All swatch interactions available via Tab and arrow keys |
| 2.4.7 Focus Visible | AA | :focus-visible outline on all interactive swatch elements |
| 2.3.3 Animation from Interactions | AAA | prefers-reduced-motion disables transitions |
| 4.1.2 Name, Role, Value | A | aria-label provides accessible name; radio inputs expose role and checked state natively |
| 4.1.3 Status Messages | AA | aria-live="polite" region announces selection changes without moving focus |
To verify accessibility on your store: