Why Map Accessibility Matters
Interactive maps pose profound accessibility challenges:
- Visual interfaces exclude screen reader users
- Mouse-driven interactions exclude keyboard users
- Color-dependent data excludes colorblind users
- Complex interfaces overwhelm stressed users
Every advocacy map must be fully usable by people with disabilities.
Screen Reader Compatibility
The Canvas Problem
WebGL-based maps (MapLibre) render to a single <canvas> element that is completely opaque to screen readers.
Leaflet Advantage
Leaflet's DOM-based architecture creates actual HTML elements:
// Markers become accessible DOM elements
L.marker([lat, lng], {
alt: 'Permanent Border Patrol checkpoint on I-8 East',
title: 'I-8 Checkpoint'
}).addTo(map);
MapLibre ARIA Injection
// Label the canvas for screen readers
map.getCanvas().setAttribute(
'aria-label',
'Interactive map showing immigration enforcement facilities and checkpoints across the United States'
);
// Mark as application role
map.getCanvas().setAttribute('role', 'application');
Map Description
Provide context before the map:
<div id="map-description" class="sr-only">
This map displays 47 ICE detention facilities and 23 Border Patrol
checkpoints. Use keyboard controls to navigate markers.
Press Enter on any marker for details.
</div>
<div id="map" aria-describedby="map-description"></div>
Keyboard Navigation
Required Interactions
| Key | Action |
|---|---|
| Tab | Move between markers |
| Enter/Space | Open popup |
| Escape | Close popup |
| Arrow keys | Pan map |
| +/- | Zoom |
Implementation
// Make markers focusable
markers.forEach((marker, index) => {
const element = marker.getElement();
element.setAttribute('tabindex', '0');
element.setAttribute('role', 'button');
element.setAttribute('aria-label', marker.options.title);
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
marker.openPopup();
e.preventDefault();
}
});
});
Focus Management
// Return focus after popup closes
marker.on('popupclose', () => {
marker.getElement().focus();
});
// Trap focus in popup
popup.on('open', () => {
const firstLink = popup.getContent().querySelector('a, button');
if (firstLink) firstLink.focus();
});
Skip Link
<a href="#after-map" class="skip-link">
Skip map and view facility list
</a>
<div id="map"></div>
<div id="after-map">
<!-- Alternative content -->
</div>
Color Accessibility
WCAG Requirements
| Element | Minimum Contrast |
|---|---|
| Text (normal) | 4.5:1 |
| Text (large) | 3:1 |
| UI components | 3:1 |
| Graphical objects | 3:1 |
Testing Tools
- Contrast Checker: https://webaim.org/resources/contrastchecker/
- Color Oracle: Desktop colorblindness simulator
- WAVE: Browser accessibility extension
Colorblind-Safe Palettes
| Purpose | Primary | Alternative |
|---|---|---|
| Danger (checkpoints) | #D32F2F | + triangle icon |
| Safety (sanctuary) | #388E3C | + circle icon |
| Neutral (facilities) | #1976D2 | + square icon |
Never Rely on Color Alone
function getMarkerIcon(type) {
const icons = {
checkpoint: {
color: '#D32F2F',
shape: 'triangle',
pattern: 'striped'
},
sanctuary: {
color: '#388E3C',
shape: 'circle',
pattern: 'solid'
}
};
return L.divIcon({
className: `marker marker-${icons[type].shape}`,
html: `<svg class="pattern-${icons[type].pattern}">...</svg>`
});
}
Pattern Fills
/* Stripes for colorblind differentiation */
.marker-danger {
background: repeating-linear-gradient(
45deg,
#D32F2F,
#D32F2F 5px,
#B71C1C 5px,
#B71C1C 10px
);
}
Cognitive Accessibility
Reduce Cognitive Load
Users under stress have reduced cognitive capacity.
| Principle | Implementation |
|---|---|
| Minimize options | Show essential layers only |
| Clear hierarchy | One primary action |
| Familiar patterns | Standard map interactions |
| Forgiving errors | Easy to reset/undo |
Progressive Disclosure
<!-- Initial view: simple -->
<div class="map-controls">
<button id="find-nearest">Find Nearest Resource</button>
</div>
<!-- Advanced: hidden until needed -->
<details>
<summary>More Options</summary>
<div class="advanced-filters">
<!-- Complex filters -->
</div>
</details>
Clear Loading States
<div class="map-loading" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p>Loading map data...</p>
<p class="loading-percent">45% complete</p>
</div>
Error Recovery
function handleError(error) {
const message = document.createElement('div');
message.className = 'error-message';
message.setAttribute('role', 'alert');
message.innerHTML = `
<h3>Unable to load map</h3>
<p>This might be a temporary issue.</p>
<button onclick="location.reload()">Try Again</button>
<a href="/facilities-list/">View as List</a>
`;
container.appendChild(message);
}
Alternative Formats
Data Table Alternative
<div class="table-alternative">
<h2>Facility Data</h2>
<table>
<caption>
ICE detention facilities sortable by name, state, or capacity
</caption>
<thead>
<tr>
<th scope="col">
<button>Name</button>
</th>
<th scope="col">State</th>
<th scope="col">Capacity</th>
</tr>
</thead>
<tbody>
<!-- Data rows -->
</tbody>
</table>
</div>
Text Descriptions
function generateTextDescription(facilities) {
const byState = groupByState(facilities);
let description = 'Facility Summary:\n\n';
Object.entries(byState).forEach(([state, list]) => {
description += `${state}: ${list.length} facilities\n`;
list.forEach(f => {
description += ` - ${f.name} (${f.type}, capacity: ${f.capacity})\n`;
});
});
return description;
}
Touch Accessibility
Touch Target Sizes
/* Minimum 44x44px touch targets */
.map-marker {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.popup-link {
display: inline-block;
padding: 12px 16px;
min-height: 44px;
}
Gesture Alternatives
// Provide button alternatives to gestures
const controls = L.control({ position: 'bottomright' });
controls.onAdd = () => {
const div = L.DomUtil.create('div', 'gesture-alternatives');
div.innerHTML = `
<button aria-label="Zoom in" onclick="map.zoomIn()">+</button>
<button aria-label="Zoom out" onclick="map.zoomOut()">-</button>
<button aria-label="Reset view" onclick="map.setView(center, zoom)">⌂</button>
`;
return div;
};
controls.addTo(map);
Testing Checklist
Automated Testing
- [ ] Run aXe or WAVE on map page
- [ ] Check color contrast ratios
- [ ] Validate HTML semantics
Manual Testing
- [ ] Navigate with keyboard only
- [ ] Test with screen reader (NVDA, VoiceOver)
- [ ] Test with display zoom at 200%
- [ ] Test with colors inverted
- [ ] Test with motion reduced
Screen Reader Testing
| Reader | Platform | Test Method |
|---|---|---|
| NVDA | Windows | Free download |
| VoiceOver | macOS/iOS | Built-in |
| TalkBack | Android | Built-in |
| JAWS | Windows | Commercial |
Related Resources
- Interactive Features - UX patterns
- Libraries Comparison - DOM vs Canvas
- Mobile Design - Mobile accessibility
- WCAG Guidelines - Full standards