Rubik Variant Images & Swatch

Accessibility Rubik Variant Images & Swatch

← Back to documentation

Overview

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.

Keyboard navigation

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.

Focus indicators

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.

Customizing the focus ring

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.

Tooltips on keyboard focus

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.

Screen reader support

Every swatch includes ARIA attributes that give screen reader users clear context about each option.

Option labels

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.

Selection announcements

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.

Decorative elements hidden

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.

Semantic HTML structure

The swatch component uses proper semantic HTML that screen readers understand natively:

Focus restoration

When 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.

Reduced motion

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.

Technical reference

This section documents the accessibility implementation at a technical level, for developers integrating with or extending the swatch component.

ARIA attributes on radio inputs

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 %}.

Live region for announcements

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 overlay hidden from assistive technology

<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 indicator CSS

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.

Reduced motion CSS

@media (prefers-reduced-motion: reduce) {
    .rubik-swatch__label,
    .rubik-swatch__image,
    .rubik-swatch__tooltip,
    .rubik-swatch__dropdown {
        transition-duration: 0.01ms !important;
    }
}

Focus restoration implementation

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).

Shadow DOM considerations

The swatch component renders inside a Shadow DOM (<rubik-swatch> custom element with mode: 'open'). This has several accessibility implications:

Compliance summary

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

Testing accessibility

To verify accessibility on your store:

Related guides