Why the toggle exists
The valvewarehouse.com.au accessibility statement claims WCAG 2.2 Level AAA conformance via the supplemental-version pathway permitted by §5.2.5 — the trade voice handles the design system's colour and interaction criteria, the Easy Read voice handles the content-level criteria (§3.1.3 unusual words, §3.1.4 abbreviations, §3.1.5 reading level).
For that claim to hold, the Easy Read version of every page must be reachable from a mechanism that is itself accessible — keyboard operable, screen-reader friendly, working without JavaScript, persistent across pages, bookmarkable, AAA-contrast in every state. The toggle is that mechanism.
Toggle behaviour, live
Click the Easy read pill in the top bar of this page. The sample below
flips between voices. The URL updates to ?voice=plain.
Refresh — the choice persists. Open this page in a new tab — the choice persists there
too because it's stored in a session cookie.
Brass ball valve, ½" BSP
DR brass body, full-bore lever, 20 bar WP, WaterMark certified to AS/NZS 3718.
In stock — 1,240 units. Dispatched same-day if ordered before 2pm AEST.
Brass ball valve · half-inch BSP
The body is dezincification-resistant brass (DR brass). The lever opens the full size of the pipe. It can hold pressure up to 20 bar. It is WaterMark certified for Australian plumbing under standard AS/NZS 3718.
We have 1,240 of these in stock. If you order before 2 pm today, we will send it today. (AEST is Australian Eastern Standard Time.)
Both content blocks live in the page DOM at the same time. The currently-active voice is
determined by the data-voice attribute on the
<html> element, set by the script in the page head before
render to avoid a flash of wrong content.
How the active voice is determined
When the user lands on a page, the script checks three sources in priority order. The first match wins.
URL parameter
?voice=plain sets Easy Read.
?voice=trade or no parameter sets trade voice. URL always
wins so a bookmarked or shared link always renders the voice it points to.
Session cookie
vwa_voice=plain in the user's session cookies sets Easy
Read. The cookie is written when the user toggles, persists across pages until the
browser closes.
Default
No URL parameter, no cookie — the page renders in trade voice. The vast majority of first-time visitors land here, which is intentional.
What happens without JavaScript
The toggle is built around real <a> tags with real
href values. With JavaScript disabled, clicking either pill
navigates to the same URL with ?voice=plain or
?voice=trade appended — a normal page load. The server reads
the parameter and renders the correct voice.
For the static-HTML mockups in this repository, there is no server, so the no-JS
fallback is implemented client-side: a small script in the page <head>
reads the URL parameter before the body renders and sets the
data-voice attribute on the <html>
element. Even this script runs synchronously and blocks paint, so there is no flash of
wrong content.
Click intercepted, data-voice attribute updated, URL
rewritten via history.replaceState, cookie set. No page
reload. Instant switch.
Link followed normally. New URL has ?voice=plain. Server
(or static fallback script) reads it and renders the correct voice. Cookie cannot
be set without JS, so persistence is URL-only.
Copy-paste the toggle
This block goes in the top bar of every page that participates in the two-voice system.
<!-- Easy Read toggle. Place in top bar, right of phone number. --> <div class="voice-toggle" role="group" aria-label="Page voice — choose between trade language and plain language" > <a href="?voice=trade" class="voice-toggle__option" aria-current="true" data-voice-set="trade" >Trade</a><a href="?voice=plain" class="voice-toggle__option" data-voice-set="plain" >Easy read</a> </div>
Two notes on the markup. First — the closing tag of the first link sits directly against
the opening tag of the second, with no whitespace between them. This eliminates the
inline-block whitespace gap that would otherwise appear between the two pills. Second
— the aria-current="true" attribute is the SR signal for
which side is currently active, and is also what the CSS uses to style the active state
(no separate .is-active class needed).
Styling the toggle
The toggle uses three CSS attribute selectors to carry both meaning and presentation in
a single source of truth — the aria-current attribute.
.voice-toggle {
display: inline-flex;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
overflow: hidden;
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 0.6875rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.voice-toggle__option {
padding: 7px 12px;
color: rgba(255, 255, 255, 0.75);
text-decoration: none;
min-height: 28px;
display: inline-flex;
align-items: center;
}
.voice-toggle__option:hover {
color: var(--vwa-brass);
}
.voice-toggle__option[aria-current="true"] {
background: var(--vwa-brass);
color: var(--vwa-navy);
font-weight: 500;
}
Toggle script
Two scripts. The first runs in <head> before the body
renders to set the initial voice without a flash. The second runs at end of body to
attach the click handlers.
Head script. Synchronous, blocking, runs once before paint.
// Apply voice before paint to prevent FOUC. (function () { const params = new URLSearchParams(location.search); const fromUrl = params.get("voice"); const fromCookie = document.cookie.match(/(?:^|; )vwa_voice=([^;]+)/); const voice = fromUrl || (fromCookie && fromCookie[1]) || "trade"; document.documentElement.dataset.voice = voice; })();
Body script. Attaches the click handlers, updates state, writes cookie.
(function () { const toggle = document.querySelector(".voice-toggle"); if (!toggle) return; const options = toggle.querySelectorAll(".voice-toggle__option"); function setVoice(voice) { // Update DOM document.documentElement.dataset.voice = voice; // Update aria-current on options options.forEach((opt) => { const isActive = opt.dataset.voiceSet === voice; opt.setAttribute("aria-current", isActive ? "true" : "false"); }); // Update URL without reload const url = new URL(location.href); if (voice === "plain") { url.searchParams.set("voice", "plain"); } else { url.searchParams.delete("voice"); } history.replaceState({}, "", url.toString()); // Persist via session cookie const maxAge = voice === "plain" ? "" : "; max-age=0"; document.cookie = `vwa_voice=${voice}; path=/; SameSite=Lax${maxAge}`; // Announce to screen readers via live region (added below) const liveRegion = document.getElementById("voice-announcer"); if (liveRegion) { liveRegion.textContent = voice === "plain" ? "Easy read mode on. Page content is now in plain language." : "Trade mode on. Page content is now in trade language."; } } options.forEach((opt) => { opt.addEventListener("click", (event) => { event.preventDefault(); setVoice(opt.dataset.voiceSet); }); }); })();
What makes the toggle AAA
-
Keyboard operable. Both options are real
<a>tags — Tab to focus, Enter to activate. No custom keyboard handling needed. -
Focus visible (§2.4.13). The site-wide
*:focus-visiblerule applies a 3-pixel navy ring with 2-pixel white offset. Inside the dark top bar, the ring uses brass on navy at 7.47:1 — exceeds the 3:1 §2.4.13 threshold. - Target size (§2.5.8 enhanced). Each pill option is 28px tall — exceeds the 24×24px AA minimum. The clickable area extends to the full padding box.
-
State communicated to assistive tech.
aria-current="true"on the active option is read by screen readers as "current page" or "current item" depending on the AT. -
Group labelled. The wrapping
role="group"andaria-labeltell screen-reader users what the two options represent before they reach them. -
Voice change announced. A visually-hidden ARIA live region
(
aria-live="polite") announces the voice change after the user activates a toggle. -
No-JS fallback. Real
hrefvalues mean the toggle works as a navigation link even when scripting is disabled. -
Reduced motion respected. The toggle has no animations beyond a 0.15s
colour transition. Within the 0.2s motion-system threshold, so
prefers-reduced-motiondoesn't need a special override.
Adding the toggle to a page
- Place the
.voice-togglemarkup in the top bar. -
Add the
.voice-toggleand.voice-toggle__optionCSS to the brand override layer. - Add the head script before any other content so it runs before paint.
- Add the body script before
</body>. -
Wrap each piece of voice-divergent content in either
data-voice-block="trade"ordata-voice-block="plain". -
Add the visually-hidden live region somewhere in the page for screen-reader
announcements:
<div id="voice-announcer" class="visually-hidden" aria-live="polite" aria-atomic="true"></div>. -
QA with JavaScript disabled — ensure the
?voice=plainlink path still works. - QA with NVDA and VoiceOver — both options announced, current state readable.