Emergency Hotline: Call 1-844-363-1423 (United We Dream Hotline)
ICE Encounter

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