Skip to content

Accessibility

Accessibility in Signal is mostly structural rather than added on: native elements where native elements exist, contrast that’s measured rather than assumed, and motion that’s gated twice.

This page is what the theme does. It isn’t a compliance certificate — your content, your apps and your own edits all affect the result.

WhatElement usedWhat comes free
Size guide, image zoom, comparison matrix, promo popup, age gate<dialog> + showModal()Focus trap, Escape, top-layer stacking, backdrop
Detail and spec accordions<details> / <summary>Keyboard operation, in-page find, open/closed state
Country, currency, language<select> in a real formPlatform picker, keyboard, screen-reader semantics
Option selection<input type="radio"> in <label>Arrow-key navigation within the group
Fit finder questions<fieldset> / <legend>Options announced as a group with their question
Variant fallback<select name="id"> in <noscript>A working product form with no JavaScript

Every modal in the theme is a real <dialog>. The focus trap is the browser’s, which means it’s correct — including for cases a hand-rolled trap usually gets wrong, like a focusable element added to the dialog after it opened.

Colours are merchant settings, so the theme can’t rely on the palette it shipped with. Three tokens are walked in a bounded loop until they clear 4.5:1 against the ground they’ll actually sit on:

  • Muted text (--mute) is tested against both grounds — the page and the deeper plane — and nudged toward the ink until both pass. The lighter ground is the binding constraint, and testing only the darker one is how a preset shipped muted text at 4.40:1.
  • The accent used as text (--accent-ink) is pushed away from the page ground until it clears — lightened on a dark ground, darkened on a light one.
  • The accent on the inverted plane (--accent-on-plane) is measured the same way against the light plane, rather than using a fixed mix that failed at 3.22:1 on one of the presets.

And text on an accent fill picks whichever neutral actually measures better, rather than assuming white:

--on-accent: {% if ct_dark > ct_white %}#1a1a1a{% else %}#ffffff{% endif %};

A single global rule, with no outline: none anywhere to undo it:

:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}

Custom controls — swatch chips, size pills, subscription rows, the gift-wrap checkbox, filter checkboxes — each restore it explicitly on their own styled element, because hiding the real input is what breaks focus rings in most themes. Signal hides the input and moves the ring to the thing you can see.

Selected states never rely on colour alone. A chosen swatch gets an outline and an offset; a saved wishlist item gets a filled icon; an unavailable size gets a diagonal hatch as well as dimmed text.

Motion is gated twice: by Theme settings → Motion → Enable motion and by the visitor’s own prefers-reduced-motion. Either one being off disables reveals, counters, the ticker, the fly-to-bag animation, drawer transitions and smooth scrolling.

Specifically:

  • prefers-reduced-motion: reduce neutralises reveal and transition rules in CSS directly, so it applies even before the script runs.
  • Autoplay on carousels doesn’t start at all under reduced motion — and when it does run, it stops on hover and on focusin, because an autoplaying carousel must stop for anyone actually reading it.
  • The back-to-top button jumps rather than smooth-scrolls, and sends focus somewhere sensible rather than leaving it on a button that’s about to fade out.
  • The loading overlay is never shown to a visitor who asks for reduced motion.

Overlays lock page scroll with a counted lock, so stacked overlays — quick view over a cart drawer — can’t have the first one closing unlock the page while another is still open. The scrollbar’s width is added back as padding so the page doesn’t jump sideways. Every <dialog> releases the lock on close, however it was closed.

The viewport meta uses viewport-fit=cover, so the page paints into a display cutout. The page margin token then keeps content clear of the notch and the home indicator:

--m: max(14px, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));

Taking the maximum means the gutter never falls below the device’s own safe area, so a landscape notch can’t clip the header or a full-bleed section — and on a device with no cutout the insets are zero and your margin setting applies unchanged.

  • A skip link is the first focusable element on the page, visually hidden until focused, jumping to #main.
  • <main id="main"> wraps page content; the header and footer are their own section groups.
  • Breadcrumbs are a <nav aria-label="Breadcrumb"> with aria-current="page" on the final rung.
  • The breadcrumb’s middle rung falls back to the product’s own first collection when the shopper didn’t arrive through one, so a visitor from an ad gets “Home / Jackets / Tactical Shell” rather than “Home / Tactical Shell”.
  • [hidden] { display: none !important; } is declared globally — without it, any class that sets a display beats the browser’s own [hidden] rule and toggling el.hidden silently does nothing, leaving content visible that the accessibility tree reports as hidden.
SurfaceAnnouncement
Cart toastrole="status", aria-live="polite" — switched to assertive for errors, because an error should interrupt and a confirmation shouldn’t
Back-in-stock confirmationrole="status"
Fit finder resultrole="status", aria-live="polite"
Compare trayaria-live="polite"
Wishlist drawer and pagearia-live="polite", with aria-busy while loading
Countdownrole="timer" with aria-live="off" — so it’s identified without announcing every second
  • Icons are inline SVG sprites marked aria-hidden="true", with the accessible name on the control that holds them.
  • Decorative image links are tabindex="-1" and aria-hidden="true" where the adjacent text link already carries the destination, so a keyboard user gets one stop per row rather than two.
  • Product media alt text falls back to the product title when a media item has none.
  • Rating stars are hidden from assistive technology and replaced by a single aria-label giving the score, the scale and the count.
  • Wishlist hearts and compare controls are real <button>s with aria-pressed, so the toggle state is announced rather than inferred from a colour.
  • Their labels name the product — “Save Tactical Shell for later” — so a column of hearts isn’t a column of identical “Save” buttons.
  • Quantity steppers have explicit labels, and the number input has a visually hidden <label> as well.