# Maps that are the only way to find a location

Source: https://easeweb.dev/learn/interactive-maps
Topics: Drag and drop > Maps have a text alternative and keyboard controls (WCAG 1.1.1, 2.1.1, 2.5.7)
The fix: Write the places on the page as a list beside the map, with each name, address, landmark and hours. Then either keep the map, make its pins buttons named after their places and add buttons that move it; or replace it with a still image that shows every place, with directions links in the list. A list alone leaves the map moving only by drag.
Test with: keyboard, screen reader, mouse, touch, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html https://www.w3.org/WAI/tutorials/images/complex/ https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F108 https://leafletjs.com/reference.html

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A coffee company's "Find a café" page has a heading, a line that says there are three cafés in town, and a map. The map is the design's centrepiece: a drawing of the town, with a pin on each café. Two pins are in view, one by the harbour and one on the market square; the third café, at the old mill, is off the right-hand edge, and a drag across the map brings it into view. A click on a pin opens a small card at the foot of the map with the café's address and opening hours.

Press Tab and focus goes from the top of the page straight to the Holiday opening hours link under the map. The pins are never a stop, so the card never opens, and nothing moves the map. A screen reader reading down the page says "Find a café, heading level 2", "Three cafés in town", and then the link. It never says which cafés, or where.

## Why it fails

The map is a picture that carries information: where each café is. Its image has `alt=""`, which tells assistive technology it is decoration, and the pins drawn over it are empty `div` elements. The only text about the cafés is the card a click on a pin writes into. So the information the map carries has no text alternative, which fails 1.1.1. WAI's images tutorial counts maps showing locations as complex images: the short alt text says what the image is, and a longer text gives what it shows, best written on the page itself.

The pins are `div` elements with click handlers. The browser gives a `div` no Tab stop and no keys, so a keyboard can't open a café's card, and the map's view moves only under a dragging pointer, with no key that moves it. Finding a café's details, and seeing the part of the town off the edge, are functions of the page that don't work from a keyboard, which fails 2.1.1.

The only way to move the map is a drag, which fails 2.5.7. Moving a map's view doesn't need one: the result doesn't depend on the path the pointer takes. The Understanding page for 2.5.7 gives this very case as one of its examples of a pass: "a map allows users to drag the view of the map around, and the map has up/down/left/right buttons to move the view as well." It also says that keyboard support alone doesn't settle the criterion: someone on a touch screen, with a head pointer or with a mouse they can't hold down needs something to click or tap. The alternative doesn't have to be part of the map, "so long as the functionality is equivalent", which is why a list of the cafés counts for finding them, but not for seeing the streets around them.

## Who is affected

On a phone the map fills the screen and shows even less of the town, so more cafés start off its edge. And a café finder is often opened on the way somewhere, when an address in words is also what people paste into their own map app.

## What automation and AI miss

A scanner sees an image with `alt=""` and three empty `div` elements: nothing that needs a name, and nothing that claims to be a control. So the Before breaks no rule it knows. EaseWeb's audit sends a review lead for each map drawn by a common map library, such as Leaflet, Mapbox GL, MapLibre, Google Maps or OpenLayers, and every map embedded from a map service, saying how many of its pins have no name or the library's generic one, and how many take no focus. Only a person can tell whether the places are also written on the page, and whether the map moves without a drag. A map drawn by the site's own code, like this one, has nothing that marks it as a map, so the audit doesn't find it: a person has to look. Asked to make the map accessible, a code assistant often adds `aria-label="Map"` to the container and `role="button"` with `tabindex="0"` to the pins. The pins can then be reached, but each is read as an unnamed button, the places are still written nowhere, and the map still moves only by dragging.

## Before: a map that is the only place the cafés are

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">Find a café</h2>
  <p>Three cafés in town.</p>
  <div class="map" id="map">
    <div class="map-view">
      <img class="map-tiles" src="/maps/town.png" alt="">
      <div class="pin" data-shop="harbour" style="left: 20%; top: 50%"></div>
      <div class="pin" data-shop="market" style="left: 42%; top: 58%"></div>
      <div class="pin" data-shop="mill" style="left: 84%; top: 30%"></div>
    </div>
    <div class="map-card" id="map-card" hidden></div>
  </div>
  <a href="/holiday-hours">Holiday opening hours</a>
</section>
```

```js
const map = document.getElementById('map');
const view = map.querySelector('.map-view');
const card = document.getElementById('map-card');
const SHOPS = {
  harbour: 'Harbour Street: 12 Harbour Street, by the ferry terminal. Daily 7:00–18:00.',
  market: 'Market Square: 3 Market Square, opposite the town hall. Daily 8:00–17:00.',
  mill: 'Old Mill: The Old Mill, Mill Lane, beside the river. Weekends 9:00–16:00.'
};
view.addEventListener('pointerdown', startPan);
// pointermove moves .map-view with the pointer; pointerup ends the pan
for (const pin of map.querySelectorAll('.pin')) {
  pin.addEventListener('click', () => {
    card.textContent = SHOPS[pin.dataset.shop];
    card.hidden = false;
  });
}
```

- The image is marked as decoration.
- The pins are empty.
- The addresses are in the script until a click puts one in the card, so the cafés are written nowhere on the page.
- The pins take no focus and no keys.
- The map moves only while a pointer drags it, so the Old Mill can't be found without a drag.

## After: a list of the cafés, pins as buttons, and buttons to move the map

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">Find a café</h2>
  <p>Three cafés in town.</p>
  <div class="map" id="map">
    <div class="map-view">
      <img class="map-tiles" src="/maps/town.png" alt="">
      <button type="button" class="pin" data-shop="harbour" style="left: 20%; top: 50%" aria-label="Harbour Street"></button>
      <button type="button" class="pin" data-shop="market" style="left: 42%; top: 58%" aria-label="Market Square"></button>
      <button type="button" class="pin" data-shop="mill" style="left: 84%; top: 30%" aria-label="Old Mill"></button>
    </div>
    <div class="map-card" id="map-card" hidden></div>
    <div class="map-pan" role="group" aria-label="Move the map">
      <button type="button" data-pan="up" aria-label="Up">↑</button>
      <button type="button" data-pan="left" aria-label="Left">←</button>
      <button type="button" data-pan="right" aria-label="Right">→</button>
      <button type="button" data-pan="down" aria-label="Down">↓</button>
    </div>
  </div>
  <ul class="cafes">
    <li>
      <h3>Harbour Street</h3>
      <p>12 Harbour Street, by the ferry terminal. Daily 7:00–18:00.</p>
      <a href="https://maps.example/?q=12+Harbour+Street">Directions to Harbour Street</a>
    </li>
    <li>
      <h3>Market Square</h3>
      <p>3 Market Square, opposite the town hall. Daily 8:00–17:00.</p>
      <a href="https://maps.example/?q=3+Market+Square">Directions to Market Square</a>
    </li>
    <li>
      <h3>Old Mill</h3>
      <p>The Old Mill, Mill Lane, beside the river. Weekends 9:00–16:00.</p>
      <a href="https://maps.example/?q=The+Old+Mill+Mill+Lane">Directions to Old Mill</a>
    </li>
  </ul>
  <a href="/holiday-hours">Holiday opening hours</a>
</section>
```

```js
const map = document.getElementById('map');
const view = map.querySelector('.map-view');
const card = document.getElementById('map-card');
const SHOPS = {
  harbour: 'Harbour Street: 12 Harbour Street, by the ferry terminal. Daily 7:00–18:00.',
  market: 'Market Square: 3 Market Square, opposite the town hall. Daily 8:00–17:00.',
  mill: 'Old Mill: The Old Mill, Mill Lane, beside the river. Weekends 9:00–16:00.'
};
let [x, y] = [0, 0];
// Every way of moving the map ends here: the drag and the buttons.
function moveTo(toX, toY) {
  [x, y] = [toX, toY]; // kept within the edges of the map
  view.style.translate = `${x}px ${y}px`;
}
const STEPS = { up: [0, 100], down: [0, -100], left: [100, 0], right: [-100, 0] };
for (const button of map.querySelectorAll('[data-pan]')) {
  button.addEventListener('click', () => {
    const [dx, dy] = STEPS[button.dataset.pan];
    moveTo(x + dx, y + dy);
  });
}
view.addEventListener('pointerdown', startPan); // the drag calls moveTo() as it moves
for (const pin of map.querySelectorAll('.pin')) {
  pin.addEventListener('click', () => {
    card.textContent = SHOPS[pin.dataset.shop];
    card.hidden = false;
  });
  // A pin reached with the keyboard brings the map round to it, so its ring is never off the edge.
  pin.addEventListener('focus', () => {
    if (pin.matches(':focus-visible')) moveTo(map.clientWidth / 2 - pin.offsetLeft, map.clientHeight / 2 - pin.offsetTop);
  });
}
```

1. Write a list of the cafés under the map, as its text alternative: each café's name as a heading, its address, the landmark that says where it is on the map ("by the ferry terminal"), its hours and a directions link. An address alone doesn't say what the map shows to someone who doesn't know the town, so the landmark is part of the alternative.
2. Keep `alt=""` on the map image: what it shows is now written right under it.
3. Make the pins native buttons named after their cafés, so Tab reaches each one, Enter and Space open its card, and voice control users can say "Click Old Mill".
4. Add Up, Down, Left and Right buttons, in a group named "Move the map", each named by the way the view goes. Each click or tap moves the view by 100 pixels, the step a short drag would make, so a few presses of Right bring the Old Mill into view.
5. Send the buttons and the drag through the same `moveTo()`. The drag stays as a shortcut.
6. When Tab reaches a pin, move the map to put it in the middle of the view, so Tab never leaves the focus ring off the edge. Without that, the browser scrolls the map's frame itself to the pin, and the map is no longer where the script thinks it is.

This fixes the Before and passes 1.1.1, 2.1.1 and 2.5.7.

## After: a list of the cafés with a still map that shows them all

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">Find a café</h2>
  <p>Three cafés in town.</p>
  <img class="map-still" src="/maps/three-cafes.png" alt="Map of the three cafés, listed below">
  <ul class="cafes">
    <li>
      <h3>Harbour Street</h3>
      <p>12 Harbour Street, by the ferry terminal. Daily 7:00–18:00.</p>
      <a href="https://maps.example/?q=12+Harbour+Street">Directions to Harbour Street</a>
    </li>
    <li>
      <h3>Market Square</h3>
      <p>3 Market Square, opposite the town hall. Daily 8:00–17:00.</p>
      <a href="https://maps.example/?q=3+Market+Square">Directions to Market Square</a>
    </li>
    <li>
      <h3>Old Mill</h3>
      <p>The Old Mill, Mill Lane, beside the river. Weekends 9:00–16:00.</p>
      <a href="https://maps.example/?q=The+Old+Mill+Mill+Lane">Directions to Old Mill</a>
    </li>
  </ul>
  <a href="/holiday-hours">Holiday opening hours</a>
</section>
```

```js
// The map is an image now: there is no pan, and no pin to open.
```

1. Replace the interactive map with an image of the whole town, drawn far enough out that all three cafés are in view with their names beside the pins. Nothing on the page moves, so there is nothing to drag and nothing a keyboard has to reach in the map.
2. Give the image alt text that says what it is and where its information is written, one of the ways WAI's images tutorial gives for a complex image.
3. Write the list of the cafés under it, which says the rest. The directions links hand the route to a map service, which a phone may open in the visitor's own map app.

This fixes the Before and passes 1.1.1, 2.1.1 and 2.5.7. It is the least code of the three, and the right choice when a page has a few places that fit in one picture; with many places, or a town too big to draw at a readable size, the interactive map earns its keep.

## After: a list of the cafés, the map left as it was

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">Find a café</h2>
  <p>Three cafés in town.</p>
  <div class="map" id="map">
    <div class="map-view">
      <img class="map-tiles" src="/maps/town.png" alt="">
      <div class="pin" data-shop="harbour" style="left: 20%; top: 50%"></div>
      <div class="pin" data-shop="market" style="left: 42%; top: 58%"></div>
      <div class="pin" data-shop="mill" style="left: 84%; top: 30%"></div>
    </div>
    <div class="map-card" id="map-card" hidden></div>
  </div>
  <ul class="cafes">
    <li>
      <h3>Harbour Street</h3>
      <p>12 Harbour Street, by the ferry terminal. Daily 7:00–18:00.</p>
      <a href="https://maps.example/?q=12+Harbour+Street">Directions to Harbour Street</a>
    </li>
    <li>
      <h3>Market Square</h3>
      <p>3 Market Square, opposite the town hall. Daily 8:00–17:00.</p>
      <a href="https://maps.example/?q=3+Market+Square">Directions to Market Square</a>
    </li>
    <li>
      <h3>Old Mill</h3>
      <p>The Old Mill, Mill Lane, beside the river. Weekends 9:00–16:00.</p>
      <a href="https://maps.example/?q=The+Old+Mill+Mill+Lane">Directions to Old Mill</a>
    </li>
  </ul>
  <a href="/holiday-hours">Holiday opening hours</a>
</section>
```

```js
const map = document.getElementById('map');
const view = map.querySelector('.map-view');
const card = document.getElementById('map-card');
const SHOPS = {
  harbour: 'Harbour Street: 12 Harbour Street, by the ferry terminal. Daily 7:00–18:00.',
  market: 'Market Square: 3 Market Square, opposite the town hall. Daily 8:00–17:00.',
  mill: 'Old Mill: The Old Mill, Mill Lane, beside the river. Weekends 9:00–16:00.'
};
view.addEventListener('pointerdown', startPan);
// pointermove moves .map-view with the pointer; pointerup ends the pan
for (const pin of map.querySelectorAll('.pin')) {
  pin.addEventListener('click', () => {
    card.textContent = SHOPS[pin.dataset.shop];
    card.hidden = false;
  });
}
```

1. Add the list from the first fix under the map, and leave the map otherwise untouched. Every café is now written on the page with where it is, a screen reader reads them all, and the cafés can be found by keyboard and with single clicks, in the list.

This is the smallest change, and it passes 1.1.1. But the map itself still moves only by dragging, and its pins still take no focus. A keyboard user, or someone who can't drag, can't see the streets around a café or bring the Old Mill into view, and a list of addresses isn't an equivalent for that, so 2.1.1 and 2.5.7 still fail for the map. It is not enough on its own: add the pin buttons and the buttons that move the map, as the first fix does, or replace the map with a still image, as the second does.

## Which option to use

The first two fixes both pass all three criteria, and which one to use depends on the page. Keep the interactive map, with the list, the pin buttons and the buttons that move it, when people use the map itself: to see what is near a café, or to compare where many places are. It is the most code, and every map component the site uses has to be checked for the same three things. Use the still map when a few places fit in one picture, as three cafés in one town do: it is the least code, nothing has to be made to work by keyboard, and the directions links send people to a map app that is built for routes. The list alone, under a map left as it was, is a first step that gives everyone the addresses, but the map still needs the buttons, or to become a still image.

## Implementation decisions

The list belongs beside the map, on the same page, in the order a person would read them, not behind a "View as list" tab or on a page of its own. The list is the thing everyone can use. A few things decide how it and the map behave on a real site:

- Order: put the list after the map in the markup, as here, or before it. Either works, as long as the cafés in the list are in the same order as their pins, and a screen reader user reading the page meets the list without having to Tab through the map.
- A pin's card: if a pin opens a card, put the card straight after the pins in the markup, as here, so Tab goes from the pins into the card when the card has links. If the card holds nothing a keyboard needs, it can stay as plain text, since the list says the same.
- Many places: with dozens of places, the pins become too many Tab stops. Keep the list, give it a search or a filter by area, and draw the pins as marks the list stands in for: elements that take no focus, hidden from assistive technology with `aria-hidden="true"`, with each place's own button in the list. Never hide a pin that can still take focus. Clusters of pins that open when clicked are single clicks, and pass.
- Zoom: if the map zooms with the scroll wheel or a pinch, add Zoom in and Zoom out buttons for the same reasons as the buttons that move it. Pinching in general is covered in [pinch, drag and tilt gestures with no button](/learn/gestures).
- Keys on the map: map libraries often make the map itself a Tab stop that the arrow keys move and the + and − keys zoom. That passes 2.1.1, but not 2.5.7, and screen readers often keep the arrow keys for reading, so draw the buttons too.
- Focus: give the pins and the buttons a focus ring at 3:1 against the map behind them, and a name that matches their place, not "Marker"; see [low-contrast form fields, icons and focus rings](/learn/non-text-contrast).
- The drag: a map that pans with a drag usually sets `touch-action: none`, so a finger on it can't scroll the page. On a phone, a map that fills the width then stops the page from scrolling under a finger, so leave a margin beside it to scroll on, or use `touch-action: pan-y`, so that an up-and-down swipe scrolls the page and the buttons move the map up and down.

Map libraries differ, and their defaults decide much of this. Leaflet, for example, makes each marker a Tab stop that Enter presses, but gives its image the alt text "Marker" unless you set `alt` to the place's name; it makes the map focusable, moved by the arrow keys and zoomed with + and −, and draws Zoom in and Zoom out buttons, but no buttons that move the map. An embedded map from a map service has its own controls, which you can't change, so the list on your page is what you control. Check what the focused pin is called and what single clicks can do, not what the library's documentation promises.

## Verify the fix

1. With a screen reader, read the page from the heading down without touching the map, and confirm every place's name, address, landmark and hours are read, and that a still map's alt text says where they are written.
2. Put the mouse aside. Press Tab through the page and confirm you can reach every place, in the list and, if the map stays, on each pin, with each pin read by its place's name.
3. If the map stays, move it with the keyboard alone, through its buttons, until the place furthest from the start is in view, and confirm the focus ring shows on every pin and button as you go.
4. With a mouse using clicks only, and with single taps on a touch screen, bring every part of the map into view and open each place's details, without dragging. With voice control, move the map by saying a button's name.
5. On a phone, and at 400% zoom, confirm the list still comes with the map, the buttons that move it are still there, and a finger on the map doesn't stop the page from scrolling.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the map library or its settings change.

## Limits

This is a starting pattern, not a guarantee for every application. The addresses, landmarks and URLs are placeholders. This guide covers a map that is the only way to find a place; the title of a map's iframe is covered in [iframes with a missing or generic title](/learn/frame-titles), alt text for a map that is only an image in [missing alt text on images that carry information](/learn/informative-images), and pinching in general in [pinch, drag and tilt gestures with no button](/learn/gestures). Routes, turn-by-turn directions, floor plans and maps that people draw on have needs of their own, and are not covered here. A map service's own embed or app is beyond your control; test what visitors meet when a directions link takes them there, with your target assistive technology.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
