Files
school_compare/docs/superpowers/specs/2026-07-02-ux-audit-notes/journey-3-compare.md
2026-07-02 11:49:39 +01:00

15 KiB
Raw Permalink Blame History

Journey 3: Building a comparison (/compare) — audit notes

Pages visited: /compare (empty state, direct entry), /compare?urns=… (populated, 13 schools), add-school modal, /school/142161-… (persistence round-trip) Viewports: 390×844 (primary), 1440×900 (+720×450 proxy for 200% reflow) Site: live https://schoolcompare.co.uk only. Read-only: searched, added/removed schools in the compare basket (client/localStorage + URL state), opened/closed the modal, hovered the chart. Submitted no data-modifying forms. selectedSchools was cleared from localStorage only to reproduce a genuine empty state and to test the share-link defect.

Task attempt log

Direct entry premise: 20% of sessions enter on /compare, so the empty state is judged as a cold arrival.

Mobile (390×844) — empty state.

  • Navigating to /compare directly first showed a persisted prior selection (The Castle School, carried in localStorage.selectedSchools from an earlier session) — the URL self-rehydrated to ?urns=136916&metric=…. This is persistence working (see Works well) but means the "empty" state is only seen by first-ever visitors. Cleared storage to capture the true empty state (j3-compare-empty-mobile.png).
  • Empty state is instructive and actionable: heading "No schools selected", body "Add schools from the home page or search to start comparing", and a primary button "+ Add Schools to Compare" that opens an in-page search modal. A cold arrival can start without leaving the page.

Mobile — building a two-school comparison. Taps counted from empty state:

  1. Tap "+ Add Schools to Compare" → opens modal, auto-focuses the "School name or postcode" search box.
  2. Type "St Mary" → live result list appears (name + LA + school type + "+ Compare" per row).
  3. Tap "+ Compare" on a result → school added; modal stays open, field clears ("Comparing 1 school" behind it).
  4. Type "Ottery St Mary" → result list.
  5. Tap "+ Compare" → second school added (?urns=142161,113105).
  6. Tap "Close modal" → reveals the comparison (j3-compare-two-schools-mobile.png).

6 distinct interactions (4 taps + 2 text-entry sequences) to a two-school comparison. Critically, both schools had to be found by name/postcode — there is no browse / "schools near this one" / "similar schools" on-ramp inside compare (ties to Journey 2 F2). A parent who knows only one school name cannot build a comparison here.

Mobile — comparison output assessment.

  • Per-metric school cards stack vertically and are fully legible; each shows name, LA, type, the selected metric value (e.g. 77.0% vs 51.0%), colour-keyed to the chart. Good on 390px.
  • Chart ("Performance Over Time"): renders on real scroll (blank in full-page captures = lazy-render artifact, confirmed by element screenshot j3-compare-chart-mobile.png). Legend labels each line by colour + full school name — colour is not the sole differentiator (WCAG 1.4.1 satisfied). canvas has role="img" but aria-label = null (no text alt).
  • "Detailed Comparison" table: wrapper 626px inside a 324px column → only Year + the first school column fit; the second school is off-screen behind horizontal scroll (overflow-x:auto, no visible scroll affordance). The whole point of the table (side-by-side) can't be seen at once on the primary viewport.
  • Metric selector carries a plain-English caption ("% meeting expected standard in reading, writing and maths") — jargon defined in place (the thing school-detail pages lacked, Journey 2 F3).

Mobile — remove / add / persistence.

  • Remove (×) is immediate: no confirm, no toast, no undo; URL updates instantly.
  • Postcode search works: "TA1 5AU" → The Castle School (advertised feature, functional).
  • Add beyond two / phase mixing: adding a secondary school to a primary comparison keeps the header at "Comparing 2 schools" but renders only the primary card; the secondary sits hidden behind a "Secondary (1)" tab with no explanation.
  • Persistence: navigated to a school detail page, then back to bare /compare — both schools restored and URL rehydrated to ?urns=142161,136916. State survives real navigation via localStorage; comparisons are URL-encoded / deep-linkable.

Desktop (1440×900).

  • Table fits with both/all school columns visible (no page overflow). Layout clean (j3-compare-two-schools-desktop.png).
  • Keyboard add/remove: "+ Add School" reachable and Enter-activatable; modal opens with focus moved into the search input; Tab reaches a result's "+ Compare" with a visible 2px solid orange (#E07256) focus outline; Enter adds it. But after adding, focus drops to document.body; after Escape (which does close the modal) focus is again on body, not returned to the "+ Add School" trigger.
  • Modal has no role="dialog" and no aria-modal — not announced as a dialog.
  • Chart tooltip (j3-compare-chart-tooltip-desktop.png): hover shows year + per-school values with colour swatch + name — mouse-only (no touch/keyboard equivalent), but the same values are present as text in the table below.
  • 200% zoom (720×450 proxy): no page-level horizontal overflow — content reflows (WCAG 1.4.10 pass); the detailed table retains its own contained horizontal scroll.

Is 32% of exits task-complete satisfaction or abandonment?

Mixed, with concrete abandonment drivers — it is not safe to read the exits as pure satisfaction.

  • Points to satisfaction: compare is a terminal "results" tool — the natural next step (visit/apply) is off-site, so a high exit share is partly expected; persistence + shareable URLs mean some exits are "saved for later"; the core output (cards + chart + table) renders and metrics are explained.
  • Points to abandonment: (a) 20% ENTER on /compare, often via a shared link — and the localStorage-overrides-URL defect (F4) silently shows a returning recipient their own schools instead of the shared ones, a confusing dead end; (b) a 2-school comparison needs 6 interactions and prior knowledge of both school names (no discovery on-ramp) — a parent with one school hits a wall; (c) on mobile the detailed table hides the second school off-screen behind an unaffordanced scroll (F1), so the comparison can look incomplete; (d) low-contrast primary buttons (F6) degrade the mobile-daylight path. F1/F4/F6 are each capable of turning an intended task-complete exit into premature abandonment, especially for the 20% arriving on shared links.

Friction points

  • F1. "Detailed Comparison" table hides the second/third school off-screen on mobile

    • Evidence: at 390px the table is 626px inside a 324px .tableWrapper (overflow-x:auto); only Year + the first school column are visible, second column cut off with no scroll affordance (j3-compare-two-schools-mobile.png).
    • Criterion violated: mobile-usability standard (horizontal scrolling of primary content is a known antipattern) + Nielsen #6 (Recognition rather than recall) — a parent must remember school A's numbers while scrolling to school B; the tool's core "side-by-side" promise is defeated on the primary viewport.
    • Argument: the whole reason a parent opens compare is to see schools next to each other; on mobile they can't, and with no scroll cue may believe the second school's data is missing and leave.
    • Severity guess: P2 (the stacked metric cards + chart do show both, softening it)
  • F2. Comparison table scroll region is not keyboard-accessible

    • Evidence: axe scrollable-region-focusable (serious, 1 node) on .ComparisonView-module__…tableWrapper; the overflowing wrapper has no tabindex.
    • Criterion violated: WCAG 2.2 2.1.1 Keyboard (A).
    • Argument: a keyboard-only parent cannot scroll the detailed table to reveal columns beyond the first school — data is literally unreachable without a mouse/touch.
    • Severity guess: P2
  • F3. Trend chart exposes no text alternative to assistive tech

    • Evidence: axe role-img-alt (serious) on canvas; aria-label = null on both mobile and desktop.
    • Criterion violated: WCAG 2.2 1.1.1 Non-text Content (A).
    • Argument: a screen-reader parent gets nothing from "Performance Over Time"; the redundant table softens this but the chart's at-a-glance trend story is lost. Recurs from Journey 2 F5 — systemic to the chart component.
    • Severity guess: P2
  • F4. Share link is silently overridden by the visitor's own localStorage

    • Evidence: navigating to /compare?urns=142161,113105 while selectedSchools held a different set resolved the page (and URL) back to the stored urns=142161,136916; only after clearing localStorage did the shared URL load its intended schools.
    • Criterion violated: Nielsen #1 (Visibility of system status) / consistency; task-flow obstruction of an advertised primary feature ("Share" button).
    • Argument: a parent shares their shortlist with a partner; if the recipient has ever used compare, they silently see their own schools with no error or notice — the shared comparison is unreproducible and the collaboration breaks. This directly hits the 20% who enter on /compare via links.
    • Severity guess: P1
  • F5. Add-school modal lacks dialog semantics and loses focus on add/close

    • Evidence: modal has no role="dialog" and no aria-modal; after a keyboard "+ Compare" add, document.activeElement = BODY; after Escape-close (Escape does dismiss it), focus is again on BODY, not the "+ Add School" trigger.
    • Criterion violated: WCAG 2.2 4.1.2 Name, Role, Value (A) + 2.4.3 Focus Order (A).
    • Argument: screen-reader users aren't told a dialog opened; keyboard users lose their place after every add (focus jumps to page top) and after closing, so building a multi-school comparison means re-tabbing from the top repeatedly — friction on the add flow every comparison depends on.
    • Severity guess: P2 (P1-candidate for keyboard/AT users building 3+ school comparisons)
  • F6. Colour-contrast failures on the primary compare controls

    • Evidence: axe color-contrast (serious, 5 nodes populated) — sample targets .btn-primary (the "+ Add School" / "+ Compare" buttons), .…phaseTabActive (active Primary/Secondary tab), .…tabActive .tabLabel (active bottom-nav label).
    • Criterion violated: WCAG 2.2 1.4.3 Contrast (Minimum) (AA).
    • Argument: the exact orange buttons and active tabs a parent must use to build and read a comparison have insufficient text contrast — hard to read on a phone in daylight (the mobile-primary audience). Recurs site-wide (Journey 1/2).
    • Severity guess: P1
  • F7. Mixed-phase selection reports "Comparing 2 schools" while showing one

    • Evidence: with a primary + a secondary selected, header reads "Comparing 2 schools" but only the primary card/chart/table render; the secondary is hidden behind a "Secondary (1)" tab with no explanatory copy.
    • Criterion violated: Nielsen #1 (Visibility of system status) & #2 (Match between system and real world).
    • Argument: a parent who added two schools sees one and may think the second was dropped; nothing explains the phase split. (Separating KS2 vs GCSE metrics is itself correct — see Works well — only the count/label is misleading.)
    • Severity guess: P3
  • F8. Remove is instant with no undo or confirmation

    • Evidence: tapping the × removed a school immediately — no confirm, no toast, no undo; URL updated instantly.
    • Criterion violated: Nielsen #3 (User control & freedom — support undo).
    • Argument: an accidental tap on the small (28px) × on mobile silently loses a school the parent may have spent a name-search to add; recovery means re-searching. A one-tap "undo" would prevent the loss.
    • Severity guess: P3

Works well — keep

  • Empty state is instructive and actionable: clear "No schools selected" message plus a "+ Add Schools to Compare" button that opens an in-page search modal — the 20% who enter cold on /compare can start immediately (answers Journey 2's "no on-ramp" concern at the compare page itself).
  • Metric selector carries a plain-English caption for the chosen metric (e.g. "% meeting expected standard in reading, writing and maths") — jargon defined in place, exactly what school-detail pages omitted.
  • Search accepts name and postcode (TA1 5AU resolved to the correct school) — the advertised postcode feature works.
  • Efficient multi-add: the modal stays open and clears the field after each add, so adding several schools needs no reopen.
  • Robust persistence + deep-linking: selection survives real navigation away and back (localStorage), and comparisons are fully URL-encoded/shareable.
  • Chart accessibility basics done right for sighted users: legend labels each series by colour and full school name (WCAG 1.4.1 satisfied); desktop hover tooltip gives precise per-year values; the table repeats those values as text.
  • Phase separation (Primary/Secondary tabs) sensibly prevents nonsensical KS2-vs-GCSE metric comparisons.
  • Desktop keyboard + reflow: visible 2px orange focus outlines on controls; modal opens with focus moved into the search field; Escape dismisses it; all columns visible without overflow; content reflows at 200% zoom with no page-level horizontal scroll (WCAG 1.4.10 pass).

Axe results

  • /compare (empty) @ 390×844: 1 violationcolor-contrast (serious, 2 nodes: active nav tab label, .btn-primary).
  • /compare?urns=142161,113105 (populated, 2 schools) @ 390×844: 3 violationscolor-contrast (serious, 5 nodes: .btn-primary, active phase tab, active nav tab), role-img-alt (serious, 1 node: canvas), scrollable-region-focusable (serious, 1 node: .tableWrapper).
  • Desktop 1440×900 populated: canvas aria-label still null (role-img-alt persists); color-contrast on .btn-primary/active tabs persists (viewport-independent).

Manual WCAG spot checks

  • Touch targets ≥24px on interactive elements (WCAG 2.5.8): pass — "+ Add School" 139×38, Share 110×40, metric select 324×41, phase tabs ~130×40. Smallest is the × remove at 28×28 — above the 24px minimum but the tightest target and below the 44px comfortable norm.
  • Keyboard: tab order / focus visibility (desktop): pass with a gap — logical order and visible 2px orange outlines on controls; modal opens with focus moved into the search box; fail on focus restoration — focus drops to body after an add and after Escape-close rather than returning to a sensible place (F5).
  • Zoom 200% text reflow (desktop, 720×450 proxy): pass — no page-level horizontal scroll; only the detailed table retains its contained scroll.

Screenshots

  • j3-compare-empty-mobile.png: true empty state (localStorage cleared) — "No schools selected" + "+ Add Schools to Compare".
  • j3-compare-addmodal-mobile.png: add-school modal with search field.
  • j3-compare-two-schools-mobile.png: mobile 2-school comparison (cards stack; detailed table shows only first school column).
  • j3-compare-chart-mobile.png: element capture proving the chart renders with a colour+name legend.
  • j3-compare-two-schools-desktop.png: desktop 2-school comparison (both columns visible).
  • j3-compare-chart-tooltip-desktop.png: mouse-hover tooltip with per-school values (3-school legend, colour + label).