Accessibility is a priority in GeoView’s development. The application follows WCAG 2.1 Level AA guidelines whenever possible, and this section highlights the key features implemented for visual, keyboard, and screen reader users.
For viewer users, start with the features overview, interaction patterns, and known limitations. For developers and reviewers, see the best practices, ARIA guidance, and review checklist.
Dynamic maps support keyboard focus and crosshair-based map interaction. Static maps do not offer these interactions or the layer, legend, details, table, and navigation controls, but retain about/notification controls and attribution/scale.
With focus inside a dynamic viewer, Ctrl+M (or Cmd+M on macOS) focuses the map element and activates, not toggles, the crosshair. Ctrl+Q exits keyboard navigation mode and deactivates the crosshair. Tabbing to another control within the viewer also deactivates it. Escape is not a general crosshair-exit shortcut; drawer editing can use it to cancel a grabbed transform. Arrow keys pan by 128px by default. Shift+ArrowUp increases the step by 10px; Shift+ArrowDown decreases it by 10px (minimum 10px).
On first tab into the viewer, users are prompted to enable keyboard navigation mode or skip the viewer. This mode enables a focus trap within the viewer; focusing the map activates crosshair-based interaction.
Keyboard focus is shown with a contrasting outline, usually with an additional halo. Placement varies by control so the indicator fits its shape and available space. Form inputs use an outline without the halo in keyboard navigation mode.
Some app-bar/nav-bar controls still suppress the standard ring; this remains an accessibility/design review item. See the contrast and visual-indicator release tests for verification, and styling best practices for developer guidance.
In keyboard navigation mode, activating a panel (e.g., Layers, Legend, Details) enables a focus trap that confines keyboard navigation within that panel. Auto-opened panels may remain outside the active trap.
Sub-panels provide similar functionality.
The Esc key intelligently handles different scenarios:
Nested content, time-slider deferred closing, and browser fullscreen can affect which action happens first. See intentional interaction patterns and fullscreen constraints.
Semantic landmarks and named regions, including the map region, support quick navigation with a screen reader.
Real-time announcements for events such as:
Interactive elements use context-specific accessible names through visible text, aria-labelledby, or aria-label:
aria-pressed with stable labels; checkbox controls use checkbox semanticsEsc key behaviour in normal mode:
When the time slider panel is open and either animation is playing OR layers are loading, pressing Esc initiates a deferred close:
Esc key behaviour in fullscreen mode:
When the time slider is in a panel fullscreen view, Esc stops any active animation and closes the fullscreen dialog. Focus returns to the surrounding panel after closing. This differs from browser fullscreen, which can consume Escape first.
Rationale:
Waiting for playback and loading to finish keeps keyboard focus stable while the panel closes.
Focus trap mutual exclusion when multiple panels open:
When a map interaction (such as clicking a feature) triggers multiple panels to auto-open simultaneously (e.g., Details panel in the app-bar and Chart panel in the footer-bar), only one panel can receive the focus trap at a time.
Close buttons depend on the layout and content, not just the active focus trap. App-bar toggle layouts and mobile layouts can show a close button even when the panel is not focus-trapped.
Behavior:
Rationale:
This mutual exclusion pattern supports multi-panel workflows:
Behavior:
Moving the mouse over the viewer after entering the keyboard-navigation flow exits keyboard mode and dismisses the entry prompt if it is still open. Pointer placement alone does not trigger this transition.
To stay in keyboard navigation mode:
Rationale:
Mouse movement is treated as a return to mouse interaction, so the viewer releases its keyboard focus trap. This explains the mode change; unresponsive buttons or broken focus traps are not intended behavior.
These sections explain shared developer conventions. Source-specific maintenance notes cover keyboard/panel focus, focus indicators, and wrapper diagnostics.
Guidance for code review:
These guidelines help catch common accessibility issues during branch review. They are advisory, not blocking — the goal is to assist developers in writing accessible code, not to prevent work-in-progress from merging.
Priority levels:
Every element id in the DOM must be unique. Duplicate IDs break label-input associations (htmlFor), invalidate ARIA relationships (aria-labelledby, aria-describedby), and cause screen readers to behave unpredictably.
/ and . are valid in HTML IDs, but require escaping when used in CSS selectorsID naming patterns (from least specific to most specific):
// Pattern 1: Element in a container
`${mapId}-${containerType}-${elementName}`
// Example: "mapA-footer-zoom-button"
// Pattern 2: Element in a panel within a container
`${mapId}-${containerType}-${panelId}-${elementName}`
// Example: "mapA-footer-layers-panel-close-button"
// Pattern 3: When a truly unique ID is required
// Prefer React's useId() for stable label/control associations
`${mapId}-${containerType}-${elementName}-${uniqueId}`;
// Example: "mapA-footer-layer-item-k8j3n2m9"
Layer paths and generated IDs:
Replacing punctuation can create duplicate IDs: group/a.b and group/a/b both become group-a-b. Use map and component-instance identifiers to keep IDs unique, and use the same ID in the associated label and ARIA references. If an ID contains special characters, escape it before using it in a CSS selector.
Clickable <div> and <span> elements do not provide button semantics, keyboard focus, or Enter/Space activation by default. Native <button> and <a href> elements come with built-in keyboard support, appropriate semantics, and browser-managed focus behaviour. Use Button, IconButton, and Link from @/ui over attaching onClick handlers to arbitrary elements.
Pick the primitive by what the element does:
| Need | Use | Notes |
|---|---|---|
| Action with a text label | Button |
Default MUI button styling |
| Icon-only action | IconButton (from @/ui) |
aria-label is required and is the fallback tooltip text when no tooltip prop is given |
| Card, tile or row that triggers an action | ButtonBase |
No padding/typography; keeps the existing card layout. Render children as component="span" (button content must be phrasing content) |
Clickable row in a List |
ListItemButton |
Put other controls in ListItem secondaryAction so they are not nested in the button |
| Navigation to a URL | Link / <a href> |
Prefer a real link over a button that calls window.open (some existing download buttons still do; migrate when touched) |
Do not hand-roll role="button" + tabIndex={0} + onKeyDown on a Box/div/Card — ButtonBase provides focus, Enter/Space activation, disabled semantics and the theme focus-visible indicator.
<ButtonBase onClick={handleClick} disableRipple sx={sxClasses.card}>
<Typography component="span">{title}</Typography>
</ButtonBase>
Preserve the keyboard focus indicator when adapting the control’s appearance to an existing layout.
Delegated listeners — An onClick on a container that reacts to clicks bubbling from real child controls (e.g. scrolling the viewer into view) does not make the container a control. Clicking its empty space must remain harmless.
The IconButton component in GeoView has built-in accessibility support. Use IconButton instead of Button when a button does not have a text label.
Its aria-label prop is required by TypeScript. The compiler catches a missing prop, but reviewers
should still check that the value is non-empty, translated, and describes the action and target.
<IconButton aria-label={t('general.close')} onClick={handleClose}>
<CloseIcon />
</IconButton>
Critical checks during code review:
aria-label, aria-labelledby, or visible text)<label> or aria-labelledby)Measure the clickable area, not just the icon. Small targets are difficult for users with motor impairments or touch screen users.
Testing tools (post-implementation):
When a focused button becomes natively disabled, focus behavior can vary by browser and the button is removed from sequential keyboard navigation. For transient states where users need to retain their position, consider aria-disabled instead.
disabled remains appropriate when a control should not be operable or in the Tab sequencearia-disabled communicates unavailability but does not prevent focus or activationaria-disabled is true. The attribute communicates unavailability but does not block activation itselfconst handleClick = (e) => {
if (isDisabled) return;
// real logic
};
<button
aria-disabled={isDisabled}
onClick={handleClick}
>
Submit
</button>
Dismissible modal dialogs, panels, and popups should respond to Esc. MUI Dialog provides Escape handling through onClose; custom components may need their own handler. Nested content handles Escape first, and the time slider can defer closing as described above.
Pattern 1: GeoView Popper component (simplest)
The GeoView Popper component from @/ui accepts handleEscapeKey directly:
import { handleEscapeKey } from '@/core/utils/utilities';
<Popper
open={isOpen}
anchorEl={anchorEl}
onClose={handleClose}
handleKeyDown={handleEscapeKey} // ← Pass directly
>
{/* content */}
</Popper>
Pattern 2: MUI Dialog dismissal
MUI Dialog handles Escape and calls onClose with the escapeKeyDown reason unless Escape dismissal is disabled. GeoView’s fullscreen dialog uses this behavior; an explicit onKeyDown is not required:
<Dialog open={isOpen} onClose={handleClose}>
{/* content */}
</Dialog>
Pattern 3: Custom keyboard dismissal
For custom content that owns dismissal and must keep a parent panel open, handle propagation at the event boundary:
import { handleEscapeKey } from '@/core/utils/utilities';
<Box
onKeyDown={(event) => {
if (event.key !== 'Escape') return;
event.stopPropagation();
handleEscapeKey(event.key, handleClose);
}}
>
{/* content */}
</Box>
Critical detail: handleEscapeKey expects a key string (event.key), not the event object itself. It does not stop propagation. If closing nested content must leave its parent open, call event.stopPropagation() in the nested handler.
What to check in code review:
handleEscapeKey instead of event.keyAvoid “loader fatigue” by not announcing every incremental progress update. Keep the named ProgressBar available to assistive technology and use a separate status message for meaningful milestones, completion, or errors when needed.
See aria-live for announcement rules, atomic messages, and the shared implementation example.
Tooltips on non-focusable elements like ListItem are not available through keyboard focus. For an action, attach the tooltip to an interactive control such as ListItemButton. For static content, provide essential information as visible text rather than a hover-only tooltip.
// Bad
<Tooltip title="Select layer xyz on the map">
<ListItem>Layer xyz</ListItem>
</Tooltip>
// Good
<Tooltip title="Select layer xyz on the map">
<ListItemButton>layer xyz</ListItemButton>
</Tooltip>
Informative images need a meaningful text alternative. Decorative images, or images whose
information is already conveyed by adjacent text, use alt="" so screen readers can ignore them.
Use translated descriptions for images authored by the application.
// Decorative or redundant with adjacent text
<img src={imageUrl} alt="" />
// Informative
<img src={imageUrl} alt={imageDescription} />
Some legend and map imagery lacks descriptive source metadata; see the non-text content limitations.
Indicates that this element represents the current item within a container or set of related elements. This is a good choice for buttons that select layers.
ARIA labels should clearly describe an element’s purpose, not just its type or location.
// Bad — role is repeated in the label, and label lacks context
<Button aria-label="Toggle visibility button" />
// Good — role is omitted, action and target are clear
<Button aria-label="Toggle visibility - Confederation to 1914" />
// Bad — identical labels across multiple elements
<Button aria-label="Learn more" />
<Button aria-label="Learn more" />
<Button aria-label="Learn more" />
// Good — each label is distinguishable
<Button aria-label="Learn more about the Confederation era" />
<Button aria-label="Learn more about WWI" />
<Button aria-label="Learn more about Industrialisation" />
GeoView’s Select wrapper requires exactly one naming prop: a non-empty visible label or
aria-label. Use translated text, or the configured attribute display label for filter controls.
The wrapper connects visible labels automatically; provide labelId only when an integration
needs a specific label ID.
<Select label={fieldLabel} value={value} onChange={handleChange} menuItems={menuItems} />
<Select aria-label={fieldLabel} value={value} onChange={handleChange} menuItems={menuItems} />
Announce loading states and progress updates using ARIA live regions
When to use aria-live
Use aria-live to announce state changes that a screen reader user might otherwise miss — such as a background process completing, an error appearing off-screen, or an asynchronous result arriving. Avoid duplicate or excessively frequent announcements:
aria-atomic
role="status" already implies aria-live="polite" and aria-atomic="true", so these attributes do not need to be repeated.Pattern
The aria-live region and progress bar serve separate concerns — the live region handles screen reader announcements, while the progress bar provides visual feedback. Keep them decoupled:
// Visually hidden live region — announces final outcome to screen readers.
// Not updated during incremental progress, only on completion or error.
<Box sx={sxClasses.visuallyHidden} role="status" aria-live="polite" aria-atomic="true">
{statusMessage}
</Box>
// Progress bar provides visual feedback during loading.
// Not announced via aria-live — user can discover via navigation.
{isLoading && (
<Box sx={sxClasses.progressBar}>
<ProgressBar aria-label={t('geolocator.loadingResults')} />
</Box>
)}
See the Legend panel for an implementation example.
The aria-pressed attribute is only relevant for toggle buttons. Use aria-pressed to communicate toggle state rather than changing the button label dynamically.
GeoView uses stable labels with aria-pressed for button toggles. A changing action label without aria-pressed is a different valid pattern, but is not the convention used here. Checkbox controls use checked state instead. See the WAI-ARIA button pattern.
// Bad — label changes on toggle, causing verbose and repetitive announcements
<IconButton
aria-label={isPressed ? 'Hide Layer Name' : 'Show Layer Name'}
/>
// Good — label stays stable, aria-pressed communicates state change
<IconButton
aria-label="Toggle visibility, Layer Name"
aria-pressed={isPressed}
/>
Note: The comma in “Toggle visibility, Layer Name” creates a natural spoken pause between the action and the target, improving clarity for screen reader users.
These are quick checks for human testers. The accessibility release tests cover keyboard navigation, focus traps, screen-reader behavior, and visual indicators.
GeoView is built to meet WCAG 2.1 Level AA. Some requirements are limited by the nature of map content, missing descriptions from external data providers, or browser behavior. This section explains those limitations, their impact, and how the relevant WCAG criteria have been interpreted. Where a limitation remains an accessibility gap rather than a permitted exception, it is identified below.
When the Fullscreen API is active, browsers commonly use Esc to exit fullscreen before the application receives the key. GeoView cannot override the browser’s exit mechanism.
Implication for UI design: If browser fullscreen and a panel are both open, users may need two Esc presses: one to exit browser fullscreen and another to close the panel. The exact behavior depends on the browser and operating system.
Note: This constraint applies only to browser fullscreen via the Fullscreen API, not panel “fullscreen views”.
Providing meaningful alternative text is not always possible for all image types within GeoView:
alt="") so assistive technology can ignore them. This follows technique H67Empty alt text does not describe informative imagery. Where information is unavailable in text, this remains an accessibility limitation rather than an H67 exception.
alt="" because no visual description is available. Adjacent names identify the items, but do not describe their colour, shape, or other visual propertiesThis limitation is related to the missing symbol descriptions discussed under SC 1.1.1 above. Where a legend relies on colour, size, or shape to connect a symbol to a map feature, users of assistive technology may not have an equivalent text description of that relationship.
Impact for users: Users can show and hide map elements by name, but may be unable to independently interpret the associated symbols. Named controls preserve access to visibility actions; they do not resolve the missing descriptions.
Non-native MUI Select controls use ARIA labelling rather than a native <label for> association. GeoView’s wrapper connects a visible label through labelId, or accepts an aria-label when no visible label is provided.
If an automated validator reports an orphaned label, check the rendered control’s accessible name and label references. A warning can be documented as a false positive when the association works correctly, including with a screen reader; do not suppress it solely because the control uses MUI.
Some content within the GeoView app may not include a lang attribute to identify passages in a language other than the page default. This is a known limitation with the following rationale:
This section helps branch reviewers spot common accessibility issues. Some checks are supported by TypeScript or ESLint; others require human judgment and runtime testing. See the related best practices.
Purpose: Assist developers during branch review by flagging patterns that commonly lead to accessibility issues. These are not merge blockers — they help catch mistakes early, but WIP and incremental improvements are welcome.
The explanations and examples are maintained once in Best Practices and ARIA Best Practices. Use this checklist as a review aid:
| Check | What to verify | Guidance |
|---|---|---|
| Icon buttons | The required label is non-empty, translated, and meaningful | Use IconButton |
| Semantic controls | Actions use buttons and navigation uses links; no hand-rolled buttons | Semantic controls |
| Toggle state | Button toggles have stable labels and aria-pressed; checkboxes use checked state |
aria-pressed |
| Disabled states | Temporary unavailability preserves focus where appropriate; activation handlers return early | Disabled states |
| Removed controls | Focus moves to a logical target when an element is removed | Removed controls |
| Escape dismissal | Built-in or custom handlers close the intended content and restore focus | Escape dismissal |
| Image alternatives | Informative images have descriptions; decorative images use empty alt text | Non-text content |
| Tooltips | Essential information is available to keyboard users | Keyboard-accessible tooltips |
| Live announcements | Necessary results are announced without duplicating focus or control feedback | aria-live |
| IDs | IDs and label references remain unique across loops and viewer instances | Unique element IDs |
| Contextual names | Labels distinguish the action and its target | aria-label |
| Atomic messages | Announcements include enough context; account for the implicit behavior of role="status" |
aria-live |
| Touch targets | Measure the clickable area against the 44×44 CSS pixel design goal | WCAG checks |
For source-search patterns and enforcement details, see the accessibility review procedure.