HurricaneMap — Voluntary Product Accessibility Template (VPAT)

Product: HurricaneMap v1.11.1
Date: 2026-09-09
Contact: GitHub Issues
Standard: WCAG 2.2 Level AA
Evaluation method: the automated suites named in the Evidence column of every row below, plus two manual audits driven in a browser on 2026-09-05 and 2026-09-08.

The Evidence column says what actually checks each claim, and says "Manual" where nothing automated does, in whole or in part. That is the point of it: a conformance report whose rows cannot be traced to something that runs is a statement of intent. 13 of the 40 rows lean on the manual audits, and npm run check:vpat counts them rather than trusting this sentence, so the number cannot drift from the table under it.

The manual audits drove the map, header, filters, storm panel, settings, glossary, statistics, this-date, compare, state, preparedness, evacuation, spatial search, table view, the poster trigger, playback and the more-actions menu, across dark, light, dark high contrast, light high contrast and emulated forced colours, at 1440x960 and 390x844. What they did not drive is listed as an open item in the project roadmap, and includes the 3D globe, the video export, the poster surface and a real Windows High Contrast pass on hardware rather than an emulated one.

This report is regenerated whenever the accessibility styles or the accessibility suite change: npm run check:vpat fails when its date is older than the newest commit touching src/styles-accessibility.css or tests/aria-regression.spec.mjs, so it cannot quietly fall behind the work again.

Conformance levels: Supports — meets the criterion | Partially Supports — some instances meet, some do not | Does Not Support — does not meet | Not Applicable — criterion does not apply

1. Perceivable

CriterionLevelConformanceNotesEvidence
1.1.1 Non-text ContentAPartially SupportsMap markers have aria-live announcements on click. Decorative images use aria-hidden. Map tile content is inherently non-textual; Section 508 data table provides equivalent text alternative.npm run test:smoke runs axe on the main view, the storm panel, the preparedness and evacuation panels and the poster, which fails on a missing alternative. The map tiles themselves are covered by the manual audits of 2026-09-05 and 2026-09-08, which drove the table view as the text equivalent.
1.2.1 Audio/VideoANot ApplicableNo audio or video content.No audio or video element exists in index.html. Nothing automated is needed to keep a criterion inapplicable, but a element added later would be caught by the axe sweeps.
1.3.1 Info and RelationshipsASupportsProper heading hierarchy (h1-h4), ARIA landmarks, role attributes on interactive elements, labeled form controls.npm run test:aria holds JSON accessibility-tree snapshots of the storm panel and the settings menu in all three locales, so a lost landmark, role or label fails as a named property rather than a diff.
1.3.2 Meaningful SequenceASupportsDOM order matches visual presentation. Panels read in logical order.Manual: the audits of 2026-09-05 and 2026-09-08 drove tab order through the map, header, filters, storm panel, statistics, this-date, compare, state, glossary, prep, evacuation, spatial search and table view. No automated check reads visual order.
1.3.3 Sensory CharacteristicsASupportsInstructions do not rely solely on shape, size, or location.Manual: the same two audits. Nothing automated can judge whether an instruction leans on shape or position.
1.4.1 Use of ColorASupportsCategory markers use dash patterns (solid/dashed/dotted/mixed) in addition to color. Colorblind palette available. Legend shows shape+color.npm run check:track-ramps checks every colour stop stays separable under simulated protanopia, deuteranopia and tritanopia. The dash patterns and the legend are Manual: the visual baselines blank the map for determinism, so they hold no map geometry.
1.4.2 Audio ControlANot ApplicableNo audio content.No audio element exists in index.html.
1.4.3 Contrast (Minimum)AASupportsText meets 4.5:1 ratio. High-contrast mode provides 7:1+ ratios and seeds from the operating system's prefers-contrast setting when no local choice exists.npm run test:smoke measures the theme contrast matrix across dark, light and both high-contrast modes, and runs axe on the default theme and on emulated forced colours. npm run test:visual holds the light and high-contrast baselines.
1.4.4 Resize TextAASupportsText resizes to 200% without loss of content. Responsive breakpoints at 720px, 640px, and 390px.Manual: the 2026-09-08 audit drove 1440x960 and 390x844. No automated check resizes text.
1.4.5 Images of TextAASupportsNo images of text used. All text is rendered as HTML.Manual: no image of text exists in the tree; the audits confirmed it.
1.4.10 ReflowAASupportsContent reflows at 320px CSS width. Mobile layout tested down to 390px.npm run test:visual holds mobile baselines at 390px including the safe-area insets, and npm run test:smoke drives the 390px panel matrix.
1.4.11 Non-text ContrastAASupportsFocus indicators, form borders, and interactive controls meet 3:1 contrast ratio. Comparison track lines on the map are measured against the five large-area OpenStreetMap fills, with each theme’s own tile filter applied and the line’s 0.85 opacity composited. The worst case is 3.80:1 in the dark theme and 4.57:1 in both the light theme and high contrast, in every case a dark green line over the forest fill. High contrast renders the same basemap and the same four track colours as the light theme, and that’s deliberate. Taking those lines up to the 7:1 this product holds its text to would make the mode worse, not better. Draining the colour out of the basemap changes nothing, because a greyscale conversion preserves luminance and a contrast ratio is a luminance calculation. Darkening the four track colours until they clear 7:1 puts all of them within a few percent of black, where a reader can no longer tell one storm from another. WCAG asks 3:1 of graphical objects at every conformance level and has no enhanced criterion for non-text contrast, so map geometry is held to 3:1 in every mode. npm run check:track-contrast measures all three modes on every build and fails if any of them drops below it.npm run check:track-contrast measures the comparison track colours against every basemap fill in every mode and fails below 3:1; npm run check:track-ramps does the same for the wind, pressure and month ramps. The note in this row records the reasoning behind holding map geometry to 3:1 rather than 7:1.
1.4.12 Text SpacingAASupportsContent accommodates increased text spacing without loss of functionality.Manual: the audits of 2026-09-05 and 2026-09-08. No automated check applies the text-spacing bookmarklet.
1.4.13 Content on Hover/FocusAASupportsTooltips are dismissible (Escape), hoverable, and persistent while hovered.npm run test:aria drives the header tooltips: dismissible with Escape, hoverable, persistent while hovered, and Escape still reaching an open panel underneath.

2. Operable

CriterionLevelConformanceNotesEvidence
2.1.1 KeyboardAPartially SupportsAll panels, dialogs, search, settings, and table view are keyboard-operable. Map markers are not individually focusable via keyboard (Leaflet limitation); the data table provides keyboard-navigable equivalent.npm run test:smoke drives the keyboard and focus contracts for the panel launcher and the dialogs; npm run test:aria drives the search listbox. The 3D globe is a Cesium canvas and is covered only by the manual audits, which is why this row is partial.
2.1.2 No Keyboard TrapASupportsEscape closes all panels. Tab moves through controls without trapping.npm run test:smoke drives the confirmation dialog, the storm panel, the preparedness and evacuation panels to Escape and asserts focus returns to the control that opened each. The poster, the 3D globe and the settings menu are Manual.
2.2.1 Timing AdjustableANot ApplicableNo time limits on content.No time limit exists. The advisory replay and the track playback are reader-driven and have no deadline.
2.2.2 Pause, Stop, HideASupportsTrack animation has pause/stop controls. Active storm badge pulse respects prefers-reduced-motion.npm run test:smoke drives the playback controls and the reduced-motion setting. The visual baselines disable animation outright, so they say nothing about it.
2.3.1 Three FlashesASupportsNo content flashes more than three times per second.No content flashes. The animations are transitions and a track playback the reader starts.
2.4.1 Bypass BlocksASupportsA keyboard-visible skip link moves focus directly to the map, supplemented by header, map, filter, and panel landmarks.npm run test:smoke drives the skip link and asserts it moves focus to the main region.
2.4.2 Page TitledASupports"HurricaneMap — 174 Years of U.S. Hurricane Landfalls"npm run test:storm-pages asserts every city page names its city in its title, and pins a sampled storm and season title; the application shell title is held by the axe sweeps in npm run test:smoke.
2.4.3 Focus OrderASupportsFocus order follows visual layout: header → filters → map → panels.npm run test:smoke asserts focus returns to its invoker for the preparedness, evacuation, table and spatial-results panels, at desktop and mobile widths. The remaining five managed panels are Manual.
2.4.4 Link PurposeASupportsAll links have descriptive text or aria-labels.npm run check:links checks every external link, and the axe sweeps in npm run test:smoke fail a link with no discernible name.
2.4.6 Headings and LabelsAASupportsSections use descriptive headings. Form controls have associated labels.npm run test:aria snapshots hold every heading and label in ten surfaces, including the storm panel and the settings menu, in all three locales. The header, the filter drawer and the map controls are Manual.
2.4.7 Focus VisibleAASupportsUniversal focus-visible rings on all interactive elements (lavender 3px outline).npm run test:smoke asserts a visible focus indicator survives in all four theme combinations.
2.4.11 Focus Not ObscuredAASupportsPanels and controls do not obscure the focused element.Manual: the audits of 2026-09-05 and 2026-09-08 drove focus through the sticky header and the panel chrome at both widths.
2.5.1 Pointer GesturesASupportsAll multi-point gestures (pinch zoom) have single-pointer alternatives (zoom buttons).No gesture is required. The map pan and zoom have keyboard and button equivalents; the audits confirmed it.
2.5.2 Pointer CancellationASupportsActions activate on click (up event), not on pointer down.Manual: the audits. No automated check distinguishes down-event from up-event activation.
2.5.3 Label in NameASupportsAccessible names include visible text for all labeled controls.npm run test:aria snapshots carry each control accessible name beside its visible label, in all three locales.
2.5.4 Motion ActuationANot ApplicableNo motion-activated functions.No motion-actuated feature exists. Nothing reads the accelerometer or the orientation sensor, and no automated check is needed to keep it that way.

3. Understandable

CriterionLevelConformanceNotesEvidence
3.1.1 Language of PageASupportslang attribute set on <html> element (en/es/ht based on user preference).npm run test:aria asserts the document language matches the selected locale for each of the three.
3.1.2 Language of PartsAASupportsStorm names and technical terms are in English regardless of locale (proper nouns).npm run check:untranslated and the ARIA snapshots cover the catalogue; the generated English biography is wrapped in a lang="en" element, asserted by npm run test:smoke.
3.2.1 On FocusASupportsNo context change on focus.Manual: the audits. Nothing focuses a control and changes context.
3.2.2 On InputASupportsFilter changes update the map immediately (expected behavior). Settings persist without page reload (except language).npm run test:smoke drives every filter control and asserts the map and the counts follow each change. That no change navigates is Manual: nothing listens for a navigation.
3.2.3 Consistent NavigationAASupportsHeader actions remain in fixed position across all states.npm run test:visual holds the shell baselines, so a moved navigation control fails as a pixel diff.
3.2.4 Consistent IdentificationAASupportsSame icons and labels used throughout for identical functions.npm run test:aria snapshots hold the same names for the same functions across panels and locales.
3.3.1 Error IdentificationASupportsInvalid year ranges are clamped. Loading failures show actionable error cards.npm run test:optional-feeds drives every one of the nineteen feeds through success, empty, error, offline, rate-limited and stale, and classifies timeout and malformed responses; npm run test:optional-feed-ui and npm run test:aria cover the words the status card puts on screen.
3.3.2 Labels or InstructionsASupportsAll form controls have visible labels or aria-labels.npm run test:aria snapshots carry every form label; axe fails an unlabelled control.

4. Robust

CriterionLevelConformanceNotesEvidence
4.1.2 Name, Role, ValueASupportsInteractive elements use semantic HTML (button, input, select) or ARIA roles (dialog, listbox, combobox, radiogroup). Settings pills use role="radio" with aria-checked.npm run test:aria JSON snapshots are exactly name, role and value for the storm panel and the settings menu, in all three locales.
4.1.3 Status MessagesAASupportsFilter count uses role="status" aria-live="polite". Map marker clicks announce to a live region. Active storm badge uses aria-live="polite".npm run test:aria drives the feed status regions and asserts aria-busy appears while a region is fetching and clears on settle or failure.

Accessibility features summary

Known limitations

This VPAT is based on the ITI VPAT 2.5 template. Last updated 2026-08-02.