Warehouse open · dispatching now · Cutoff 2pm AEST
Reference pattern · v2.0.0-AAA

The Easy Read toggle.
A reference implementation.

The toggle in the top bar of every page lets visitors switch between the trade voice (default) and Easy Read. This document is the canonical specification — copy-paste the markup, CSS and JavaScript into any page that needs it.

Section 01 · Purpose

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.

Section 02 · Try it

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.

Sample · product description

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.

Section 03 · State priority

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.

Priority 01

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.

Priority 02

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.

Priority 03

Default

No URL parameter, no cookie — the page renders in trade voice. The vast majority of first-time visitors land here, which is intentional.

Section 04 · Fallback

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.

With JavaScript

Click intercepted, data-voice attribute updated, URL rewritten via history.replaceState, cookie set. No page reload. Instant switch.

Without JavaScript

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.

Section 05 · The markup

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

Section 06 · The CSS

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;
}
Section 07 · The JavaScript

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);
    });
  });
})();
Section 08 · Accessibility

What makes the toggle AAA

Section 09 · Integration checklist

Adding the toggle to a page