NovagaitPhysical Therapy, home

Accessibility exhibit

The same site, twice

This demo follows the W3C WAI Before and After Demonstration pattern: one clinic website built twice. The before-version carries 12 realistic barriers, the kind real audits find every week. The after-version fixes every one, and a CI gate keeps them fixed.

  • 8 of 12 machine-detectable (axe-core)
  • 4 of 12 need human judgment
  • WCAG 2.2 A/AA

Audit ledgers and rescan diffDraft accessibility conformance report

Compare them live

View

Before (12 seeded barriers)

After (remediated)

Both frames load the real pages. Tab into the before-frame to feel the missing focus indicators; try the same walk in the after-frame.

The 12 barriers, one by one

Each barrier below is real on the before-pages right now. The numbering matches the audit ledgers and the seeded-violation checklist.

  1. V1. Form inputs without labels

    WCAG 3.3.2 Labels or Instructions (also 1.3.1, 4.1.2)Caught by axe: select-name (the placeholder-only text inputs PASS automated checks; catching them takes a human)

    What is broken

    Every input on the contact form is labeled only by placeholder text, which vanishes on entry and is not a programmatic label.

    <input name="phone" placeholder="Phone" />

    The fix applied

    Every field gets a visible <label> tied to the input with htmlFor/id; hints use aria-describedby.

    <label htmlFor="field-phone">Phone number</label>
    <input id="field-phone" name="phone" type="tel" autoComplete="tel" aria-describedby="hint-phone" />

    Who hits this barrier

    Screen reader users hear “edit text” with no clue what to type; voice-control users cannot name the field; everyone loses the hint once the placeholder disappears.

    See it broken (V1 on the before-site)See it fixed (V1 on the after-site)

  2. V2. Clickable divs posing as buttons

    WCAG 2.1.1 Keyboard (also 4.1.2 Name, Role, Value)Human judgment required

    What is broken

    Service cards navigate with a mouse onClick on a <div>: no role, no tabindex, no keyboard handler.

    <div onClick={() => router.push("/before/services#slug")} className="cursor-pointer">…</div>

    The fix applied

    Cards are real links: the whole affordance is an <a> (Next <Link>), natively focusable and operable.

    <Link href="/services#slug">Orthopedic rehabilitation</Link>

    Who hits this barrier

    Keyboard and switch users cannot reach the cards at all; screen readers announce them as plain text, not as something operable.

    See it broken (V2 on the before-site)See it fixed (V2 on the after-site)

  3. V3. Text contrast below 4.5:1

    WCAG 1.4.3 Contrast (Minimum)Caught by axe: color-contrast

    What is broken

    Hero headings render at roughly 2.2:1 and footer links at roughly 2.4:1 against their backgrounds.

    <p className="text-[#b6bcc9]">Feel better, move better</p>

    The fix applied

    Every text/background pair comes from design-tokens.json, where each pair is unit-tested to pass 4.5:1 (AA).

    /* tokens: light text #0F172A on #FFFFFF = 17.85:1, tested in CI */
    <h1 className="text-fg">A stronger stride starts here.</h1>

    Who hits this barrier

    Low-vision users, older users, and anyone on a phone in sunlight simply cannot read the hero or the footer links.

    See it broken (V3 on the before-site)See it fixed (V3 on the after-site)

  4. V4. Focus indicator removed globally

    WCAG 2.4.7 Focus VisibleHuman judgment required

    What is broken

    One CSS rule, *:focus { outline: none }, applied to the whole before-variant with no replacement styling.

    .before-scope *:focus { outline: none; }

    The fix applied

    A global 3px high-contrast :focus-visible outline (token-colored, 7.9:1) with offset, never removed without a replacement.

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

    Who hits this barrier

    Sighted keyboard users lose their place instantly: there is no way to see which element will activate on Enter.

    See it broken (V4 on the before-site)See it fixed (V4 on the after-site)

  5. V5. Missing and junk alt text

    WCAG 1.1.1 Non-text ContentCaught by axe: image-alt

    What is broken

    Two provider images have no alt attribute; a third carries alt=“IMG_4821.JPG”.

    <img src="/before/provider-1.svg" />
    <img src="/before/provider-3.svg" alt="IMG_4821.JPG" />

    The fix applied

    Decorative imagery is marked decorative (empty alt or aria-hidden); meaning lives in adjacent text, monograms are hidden from the tree.

    <div aria-hidden="true" class="…monogram…">MO</div>
    <h2>Maren Oduya, PT, DPT, OCS</h2>

    Who hits this barrier

    Screen reader users hear the raw filename or nothing at all where a staff photo should be described, or should be silent.

    See it broken (V5 on the before-site)See it fixed (V5 on the after-site)

  6. V6. Broken heading hierarchy

    WCAG 1.3.1 Info and Relationships (heading structure)Caught by axe: heading-order

    What is broken

    Every section heading on the before-variant is an <h4> directly under the page <h1>.

    <h1>…</h1>
    <h4 className="text-2xl">Our services</h4>

    The fix applied

    A strict h1 > h2 > h3 outline; sections are labeled by their headings via aria-labelledby.

    <h1>A stronger stride starts here.</h1>
    <h2 id="services-heading">Care built around how you move</h2>
    <h3>Orthopedic rehabilitation</h3>

    Who hits this barrier

    Screen reader users navigating by headings get a skeleton that jumps from h1 to h4: sections seem missing and the outline is meaningless.

    See it broken (V6 on the before-site)See it fixed (V6 on the after-site)

  7. V7. Keyboard trap in a popup

    WCAG 2.1.2 No Keyboard TrapCaught by axe: aria-dialog-name (the trap itself is a manual finding)

    What is broken

    A newsletter popup opens on its own, swallows Tab and Shift+Tab, ignores Escape, has a mouse-only close, and no accessible name.

    function trap(e) { if (e.key === "Tab") e.preventDefault(); }
    <div role="dialog">…<span onClick={close}>×</span></div>

    The fix applied

    The after-site has no self-opening popup at all. Its one dialog-like moment (the form success panel) moves focus in, is named by its heading, and never traps.

    /* No modal. Success panel: */
    <h2 ref={successRef} tabIndex={-1}>Request received…</h2>

    Who hits this barrier

    Keyboard users are stuck: Tab is swallowed, Escape does nothing, and the only close control is mouse-only. The session ends with the browser tab.

    See it broken (V7 on the before-site)See it fixed (V7 on the after-site)

  8. V8. Auto-playing motion that ignores reduced-motion

    WCAG 2.2.2 Pause, Stop, Hide (also 2.3.3 Animation from Interactions)Caught by axe: button-name (carousel controls; the motion itself is manual)

    What is broken

    The hero carousel auto-advances forever, overrides the reduced-motion kill switch with !important, has no pause control, and its arrows are unnamed icon buttons.

    .before-carousel-track { animation: … infinite !important; }
    <button><svg>…</svg></button>

    The fix applied

    No carousel. The hero animates once (a line-draw), fully gated behind prefers-reduced-motion; under reduced motion nothing moves.

    @media (prefers-reduced-motion: no-preference) {
      .stride-path .stride-line { animation: stride-draw 1.4s ease-out both; }
    }

    Who hits this barrier

    Vestibular-disorder users get motion they asked the OS to suppress; screen reader users hear unnamed buttons; nobody can pause the carousel.

    See it broken (V8 on the before-site)See it fixed (V8 on the after-site)

  9. V9. Touch targets under 24px

    WCAG 2.5.8 Target Size (Minimum)Caught by axe: target-size + link-name

    What is broken

    Footer social links are 16x16px icon-only anchors with no accessible name.

    <a href="…" className="block h-4 w-4"><svg>…</svg></a>

    The fix applied

    All interactive targets are at least 44px (tokens.touchTarget); icon-only controls carry accessible names or are dropped.

    /* 44px minimum enforced via min-h-11 utilities on every control */
    <Link className="inline-flex min-h-11 items-center …">Services</Link>

    Who hits this barrier

    Users with tremor or limited dexterity, and anyone on a bumpy commute, cannot reliably hit 16px icon links; screen readers announce them as nothing.

    See it broken (V9 on the before-site)See it fixed (V9 on the after-site)

  10. V10. Errors shown by color alone

    WCAG 3.3.1 Error Identification (also 1.4.1 Use of Color)Human judgment required

    What is broken

    Invalid fields get a red border. No text, no aria-invalid, no focus move, no announcement.

    className={bad.has("phone") ? "border-[#dc2626] border-2" : "border"}

    The fix applied

    An error summary (role=alert) receives focus and links each problem to its field; every field gets a text error via aria-describedby and aria-invalid.

    <div ref={summaryRef} tabIndex={-1} role="alert">…There are 2 problems…</div>
    <input aria-invalid aria-describedby="error-phone" />
    <p id="error-phone">Error: Enter a phone number…</p>

    Who hits this barrier

    Blind users hear nothing happen after submitting; colorblind users see nothing change; nobody is told what to fix.

    See it broken (V10 on the before-site)See it fixed (V10 on the after-site)

  11. V11. Sticky header hides focused elements

    WCAG 2.4.11 Focus Not Obscured (Minimum)Human judgment required

    What is broken

    The before-header is position: sticky with no scroll-padding or scroll-margin anywhere, so focused elements scroll underneath it.

    <header className="sticky top-0 z-40 …">

    The fix applied

    The after-header is not sticky; were it sticky, scroll-padding-top would reserve its height so focused elements stay visible.

    /* non-sticky header; sticky pattern requires: */
    html { scroll-padding-top: 5rem; }

    Who hits this barrier

    Keyboard users tabbing backward watch their focus disappear under the sticky header: the focused link is on screen but invisible.

    See it broken (V11 on the before-site)See it fixed (V11 on the after-site)

  12. V12. Redundant entry

    WCAG 3.3.7 Redundant EntryCaught by axe: label (the confirm fields are pseudo-labeled: visible text, no association)

    What is broken

    The form re-asks for the name and phone number the user just typed, behind visible pseudo-labels that are not programmatically associated.

    <p>Confirm your name</p>
    <input name="nameConfirm" type="text" />

    The fix applied

    Ask once. The after-form collects each fact exactly one time and says so.

    /* one name field, one phone field; copy: */
    <p>Tell us once. We will not ask you to repeat any of this when we call back.</p>

    Who hits this barrier

    Users with cognitive or motor disabilities pay double for every field; on mobile everyone does.

    See it broken (V12 on the before-site)See it fixed (V12 on the after-site)