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.
Native elements, not reimplementations
Section titled “Native elements, not reimplementations”| What | Element used | What 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 form | Platform 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.
Contrast is measured, not guessed
Section titled “Contrast is measured, not guessed”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 %};Focus is always visible
Section titled “Focus is always visible”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.
Reduced motion
Section titled “Reduced motion”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: reduceneutralises 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.
Scroll locking and overlays
Section titled “Scroll locking and overlays”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.
Safe-area insets
Section titled “Safe-area insets”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.
Structure and landmarks
Section titled “Structure and landmarks”- 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">witharia-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 adisplaybeats the browser’s own[hidden]rule and togglingel.hiddensilently does nothing, leaving content visible that the accessibility tree reports as hidden.
Live regions
Section titled “Live regions”| Surface | Announcement |
|---|---|
| Cart toast | role="status", aria-live="polite" — switched to assertive for errors, because an error should interrupt and a confirmation shouldn’t |
| Back-in-stock confirmation | role="status" |
| Fit finder result | role="status", aria-live="polite" |
| Compare tray | aria-live="polite" |
| Wishlist drawer and page | aria-live="polite", with aria-busy while loading |
| Countdown | role="timer" with aria-live="off" — so it’s identified without announcing every second |
Images and icons
Section titled “Images and icons”- 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"andaria-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-labelgiving the score, the scale and the count.
Buttons that mean something
Section titled “Buttons that mean something”- Wishlist hearts and compare controls are real
<button>s witharia-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.