Accessibility pattern · Media

Map with a text alternative

A map is a picture of where things are, so it needs the same facts as text: here every store, its address, distance and hours are also a list that is always on the page. The map itself still works without a mouse: pins are buttons in order of distance, the arrow keys move between them, and zoom and move have buttons as well as drag.

WCAG criteria
7
Keyboard rules
6
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Kesar Tea stores in Bengaluru

Distances from MG Road metro. The map is simplified, not to scale.

Show only

Stores shown: 8

Skip the map

Arrow keys move between stores. Plus and minus zoom. Drag or use the buttons to move the map.

Selected store

All stores, nearest first

  1. Kesar Tea, Indiranagar
    3.8 km
    Address
    100 Feet Road, HAL 2nd Stage
    Opening hours
    Open until 9 pm
    Has
    Seating, step-free entry
  2. Kesar Tea, Koramangala
    4.6 km
    Address
    80 Feet Road, 4th Block
    Opening hours
    Open until 11 pm
    Has
    Seating
  3. Kesar Tea, Basavanagudi
    5.4 km
    Address
    DVG Road, Gandhi Bazaar
    Opening hours
    Open until 8 pm
    Has
    Step-free entry
  4. Kesar Tea, Malleshwaram
    5.9 km
    Address
    Sampige Road, 8th Cross
    Opening hours
    Open until 9 pm
    Has
    Seating, step-free entry
  5. Kesar Tea, Jayanagar
    6.7 km
    Address
    11th Main, 4th Block
    Opening hours
    Open until 10 pm
    Has
    Step-free entry
  6. Kesar Tea, Hebbal
    8.1 km
    Address
    Bellary Road, near the flyover
    Opening hours
    Open until 8 pm
    Has
    Takeaway only
  7. Kesar Tea, HSR Layout
    8.9 km
    Address
    27th Main, Sector 1
    Opening hours
    Open until 11 pm
    Has
    Seating, step-free entry
  8. Kesar Tea, Whitefield
    15.8 km
    Address
    ITPL Main Road
    Opening hours
    Open until 10 pm
    Has
    Seating

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves through the view buttons, the filters, Skip the map, the selected store's pin, the zoom and move buttons, then the list.
Arrow Right or Arrow DownOn a pin, moves to the next store in order of distance and shows its details, wrapping at the end.
Arrow Left or Arrow UpOn a pin, moves to the previous store and shows its details.
Home or EndOn a pin, moves to the nearest or the farthest store shown.
Plus or MinusWith focus on a pin or a map button, zooms in or out; the selected pin stays in view.
Enter or SpaceOn a move button, moves the map a step; on Show on map in the list, shows the map, selects that store and moves focus to its pin.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the mapStores on the map, group. Kesar Tea, Indiranagar, open until 9 pm, button, current
Arrow Right on a pinKesar Tea, Koramangala, open until 11 pm, button, current
Open after 9 pm is checkedOpen after 9 pm, checkbox, checked. Stores shown: 4
Zoom in reaches its limitZoom in, button, unavailable
Show on map is pressed for WhitefieldKesar Tea, Whitefield, open until 10 pm, button, current
List is pressedList, toggle button, pressed

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<section class="ap-map-alternative" data-ap-map-alternative aria-labelledby="map-alt-name">
  <div class="ap-map-alternative__top">
    <div>
      <h3 class="ap-map-alternative__name" id="map-alt-name">Kesar Tea stores in Bengaluru</h3>
      <p class="ap-map-alternative__sub">Distances from MG Road metro. The map is simplified, not to scale.</p>
    </div>
    <div class="ap-map-alternative__views" role="group" aria-label="Show stores as">
      <button type="button" class="ap-map-alternative__view" data-map-view="map" aria-pressed="true">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m3 6 6-2 6 2 6-2v14l-6 2-6-2-6 2Z"/><path d="M9 4v14M15 6v14"/></svg>
        Map
      </button>
      <button type="button" class="ap-map-alternative__view" data-map-view="list" aria-pressed="false">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h11M9 12h11M9 18h11"/><path d="M4 6h.01M4 12h.01M4 18h.01"/></svg>
        List
      </button>
    </div>
  </div>

  <div class="ap-map-alternative__filters">
    <fieldset class="ap-map-alternative__fieldset">
      <legend class="ap-map-alternative__legend">Show only</legend>
      <label class="ap-map-alternative__filter"><input type="checkbox" data-map-filter="late" /> Open after 9 pm</label>
      <label class="ap-map-alternative__filter"><input type="checkbox" data-map-filter="seating" /> Seating</label>
      <label class="ap-map-alternative__filter"><input type="checkbox" data-map-filter="stepfree" /> Step-free entry</label>
    </fieldset>
    <p class="ap-map-alternative__count" role="status"><span>Stores shown:</span> <span data-map-count>8</span></p>
  </div>

  <div class="ap-map-alternative__mapview" data-map-mapview>
    <a class="ap-map-alternative__skip" href="#map-alt-list">Skip the map</a>
    <div class="ap-map-alternative__layout">
      <div class="ap-map-alternative__mapcol">
        <div class="ap-map-alternative__frame" data-map-frame>
          <div class="ap-map-alternative__world" data-map-world>
            <svg class="ap-map-alternative__ground" viewBox="0 0 600 400" preserveAspectRatio="none" aria-hidden="true" focusable="false">
              <rect class="ap-map-alternative__land" width="600" height="400"/>
              <g class="ap-map-alternative__areas">
                <path d="M24 146 98 138l6 162-76 16Z"/>
                <path d="M392 20 500 30l-8 66-104 8Z"/>
                <path d="M448 226 506 234l-6 42-54 8Z"/>
                <path d="M568 250 596 246v150h-30Z"/>
                <path d="M112 38 236 28l14 90-54 28-78-14Z"/>
                <path d="M268 8 384 14l-6 70-78 12-42-30Z"/>
                <path d="M256 136 346 126l16 88-82 16-36-38Z"/>
                <path d="M372 116 476 106l10 106-104 10Z"/>
                <path d="M506 104 594 96v136l-80-6Z"/>
                <path d="M330 244 432 234l10 88-96 10Z"/>
                <path d="M452 292 550 282l8 98-98 10Z"/>
                <path d="M206 280 314 270l12 92-110 10Z"/>
                <path d="M104 214 212 200l10 92-108 14Z"/>
              </g>
              <g class="ap-map-alternative__green">
                <ellipse cx="292" cy="166" rx="24" ry="14"/>
                <ellipse cx="232" cy="252" rx="20" ry="13"/>
              </g>
              <g class="ap-map-alternative__water">
                <ellipse cx="520" cy="262" rx="28" ry="12"/>
                <ellipse cx="340" cy="74" rx="14" ry="9"/>
                <ellipse cx="364" cy="160" rx="10" ry="16"/>
              </g>
              <g class="ap-map-alternative__roads">
                <path d="M62 200C58 92 196 22 320 24s242 50 250 166-64 192-240 196S66 316 62 200Z"/>
                <path d="M300 190 170 70M300 190 320 20M300 190l260-30M300 190l180 160M300 190l-46 182M300 190 96 262"/>
              </g>
            </svg>
            <span class="ap-map-alternative__here" data-x="50" data-y="47" aria-hidden="true"><span class="ap-map-alternative__dot"></span>You are here</span>
            <div class="ap-map-alternative__pins" role="group" aria-label="Stores on the map" aria-describedby="map-alt-keys">
          <button type="button" class="ap-map-alternative__pin" data-map-pin="indiranagar" data-x="71" data-y="40" aria-label="Kesar Tea, Indiranagar, open until 9 pm" aria-current="true">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">Indiranagar</span>
          </button>
          <button type="button" class="ap-map-alternative__pin" data-map-pin="koramangala" data-x="63" data-y="70" aria-label="Kesar Tea, Koramangala, open until 11 pm" tabindex="-1">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">Koramangala</span>
          </button>
          <button type="button" class="ap-map-alternative__pin" data-map-pin="basavanagudi" data-x="27" data-y="62" aria-label="Kesar Tea, Basavanagudi, open until 8 pm" tabindex="-1">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">Basavanagudi</span>
          </button>
          <button type="button" class="ap-map-alternative__pin" data-map-pin="malleshwaram" data-x="29" data-y="24" aria-label="Kesar Tea, Malleshwaram, open until 9 pm" tabindex="-1">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">Malleshwaram</span>
          </button>
          <button type="button" class="ap-map-alternative__pin" data-map-pin="jayanagar" data-x="44" data-y="80" aria-label="Kesar Tea, Jayanagar, open until 10 pm" tabindex="-1">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">Jayanagar</span>
          </button>
          <button type="button" class="ap-map-alternative__pin" data-map-pin="hebbal" data-x="53" data-y="17" aria-label="Kesar Tea, Hebbal, open until 8 pm" tabindex="-1">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">Hebbal</span>
          </button>
          <button type="button" class="ap-map-alternative__pin" data-map-pin="hsr" data-x="80" data-y="84" aria-label="Kesar Tea, HSR Layout, open until 11 pm" tabindex="-1">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">HSR Layout</span>
          </button>
          <button type="button" class="ap-map-alternative__pin ap-map-alternative__pin--end" data-map-pin="whitefield" data-x="92" data-y="50" aria-label="Kesar Tea, Whitefield, open until 10 pm" tabindex="-1">
            <svg class="ap-map-alternative__pinmark" viewBox="0 0 28 36" aria-hidden="true" focusable="false"><path d="M14 34S3 23.8 3 14.5a11 11 0 0 1 22 0C25 23.8 14 34 14 34Z"/><circle cx="14" cy="14.5" r="4.5"/></svg>
            <span class="ap-map-alternative__tag" aria-hidden="true">Whitefield</span>
          </button>
            </div>
          </div>
        </div>
        <div class="ap-map-alternative__controls" role="group" aria-label="Zoom and move the map">
          <div class="ap-map-alternative__ctlset">
            <button type="button" class="ap-map-alternative__ctl" data-map-zoom="in" aria-label="Zoom in" aria-keyshortcuts="+"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg></button>
            <button type="button" class="ap-map-alternative__ctl" data-map-zoom="out" aria-label="Zoom out" aria-disabled="true" aria-keyshortcuts="-"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14"/></svg></button>
            <button type="button" class="ap-map-alternative__ctl" data-map-reset aria-label="Reset view" aria-disabled="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 12a8 8 0 1 0 2.4-5.7M4 4v4.5h4.5"/></svg></button>
          </div>
          <div class="ap-map-alternative__ctlset">
            <button type="button" class="ap-map-alternative__ctl" data-map-pan="left" aria-label="Move left" aria-disabled="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m15 6-6 6 6 6"/></svg></button>
            <button type="button" class="ap-map-alternative__ctl" data-map-pan="up" aria-label="Move up" aria-disabled="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg></button>
            <button type="button" class="ap-map-alternative__ctl" data-map-pan="down" aria-label="Move down" aria-disabled="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg></button>
            <button type="button" class="ap-map-alternative__ctl" data-map-pan="right" aria-label="Move right" aria-disabled="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"/></svg></button>
          </div>
        </div>
        <p class="ap-map-alternative__keys" id="map-alt-keys">Arrow keys move between stores. Plus and minus zoom. Drag or use the buttons to move the map.</p>
      </div>
      <div class="ap-map-alternative__details" data-map-details>
        <p class="ap-map-alternative__kicker">Selected store</p>
        <div class="ap-map-alternative__detail" data-map-detail></div>
        <p class="ap-map-alternative__none" data-map-none hidden>No store matches these filters.</p>
      </div>
    </div>
  </div>

  <div class="ap-map-alternative__list" id="map-alt-list" tabindex="-1">
    <h4 class="ap-map-alternative__listname">All stores, nearest first</h4>
    <ol class="ap-map-alternative__stores">
      <li class="ap-map-alternative__store" data-map-store="indiranagar" data-tags="seating stepfree">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-indiranagar">Kesar Tea, Indiranagar</h5>
          <span class="ap-map-alternative__km">3.8 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>100 Feet Road, HAL 2nd Stage</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 9 pm</dd></div>
          <div><dt>Has</dt><dd>Seating, step-free entry</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="indiranagar" aria-describedby="map-alt-n-indiranagar">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
      <li class="ap-map-alternative__store" data-map-store="koramangala" data-tags="late seating">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-koramangala">Kesar Tea, Koramangala</h5>
          <span class="ap-map-alternative__km">4.6 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>80 Feet Road, 4th Block</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 11 pm</dd></div>
          <div><dt>Has</dt><dd>Seating</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="koramangala" aria-describedby="map-alt-n-koramangala">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
      <li class="ap-map-alternative__store" data-map-store="basavanagudi" data-tags="stepfree">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-basavanagudi">Kesar Tea, Basavanagudi</h5>
          <span class="ap-map-alternative__km">5.4 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>DVG Road, Gandhi Bazaar</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 8 pm</dd></div>
          <div><dt>Has</dt><dd>Step-free entry</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="basavanagudi" aria-describedby="map-alt-n-basavanagudi">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
      <li class="ap-map-alternative__store" data-map-store="malleshwaram" data-tags="seating stepfree">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-malleshwaram">Kesar Tea, Malleshwaram</h5>
          <span class="ap-map-alternative__km">5.9 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>Sampige Road, 8th Cross</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 9 pm</dd></div>
          <div><dt>Has</dt><dd>Seating, step-free entry</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="malleshwaram" aria-describedby="map-alt-n-malleshwaram">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
      <li class="ap-map-alternative__store" data-map-store="jayanagar" data-tags="late stepfree">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-jayanagar">Kesar Tea, Jayanagar</h5>
          <span class="ap-map-alternative__km">6.7 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>11th Main, 4th Block</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 10 pm</dd></div>
          <div><dt>Has</dt><dd>Step-free entry</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="jayanagar" aria-describedby="map-alt-n-jayanagar">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
      <li class="ap-map-alternative__store" data-map-store="hebbal" data-tags="">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-hebbal">Kesar Tea, Hebbal</h5>
          <span class="ap-map-alternative__km">8.1 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>Bellary Road, near the flyover</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 8 pm</dd></div>
          <div><dt>Has</dt><dd>Takeaway only</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="hebbal" aria-describedby="map-alt-n-hebbal">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
      <li class="ap-map-alternative__store" data-map-store="hsr" data-tags="late seating stepfree">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-hsr">Kesar Tea, HSR Layout</h5>
          <span class="ap-map-alternative__km">8.9 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>27th Main, Sector 1</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 11 pm</dd></div>
          <div><dt>Has</dt><dd>Seating, step-free entry</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="hsr" aria-describedby="map-alt-n-hsr">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
      <li class="ap-map-alternative__store" data-map-store="whitefield" data-tags="late seating">
        <div class="ap-map-alternative__storehead">
          <h5 class="ap-map-alternative__storename" id="map-alt-n-whitefield">Kesar Tea, Whitefield</h5>
          <span class="ap-map-alternative__km">15.8 km</span>
        </div>
        <dl class="ap-map-alternative__facts">
          <div><dt>Address</dt><dd>ITPL Main Road</dd></div>
          <div><dt>Opening hours</dt><dd>Open until 10 pm</dd></div>
          <div><dt>Has</dt><dd>Seating</dd></div>
        </dl>
        <button type="button" class="ap-btn ap-btn--ghost ap-map-alternative__show" data-map-show="whitefield" aria-describedby="map-alt-n-whitefield">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>
          Show on map
        </button>
      </li>
    </ol>
    <p class="ap-map-alternative__empty" data-map-empty hidden>No store matches these filters. Clear one to see more.</p>
  </div>
</section>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.1.1 Non-text Content Level A

    The map is a picture of where the stores are; the list gives every store's address, distance and hours as text, so nothing depends on seeing it.

  • 2.1.1 Keyboard Level A

    Pins are buttons, the arrow keys move between them, and zoom and move work from buttons and the plus and minus keys.

  • 2.4.3 Focus Order Level A

    Pins follow the list's order, nearest first, not the order they were drawn in or where they sit on screen.

  • 2.4.11 Focus Not Obscured (Minimum) Level AA

    When a pin gets focus at any zoom, the map moves to keep it in view rather than leaving it under the frame's edge.

  • 2.5.7 Dragging Movements Level AA

    Dragging moves the map, but the move buttons do the same with single presses, for people who cannot drag.

  • 4.1.2 Name, Role, Value Level A

    Each pin is named by its store and hours, the shown store is aria-current, and Map and List report pressed.

  • 4.1.3 Status Messages Level AA

    The number of stores that match the filters is a status message, announced without moving focus.

Usage

When to use it

Use it

  • Places people choose between, like stores, branches, venues or charging points.
  • Any map that carries information, as long as the same facts are also written as text.

Use something else

  • A map that is only decoration, like a skyline behind a heading: give it empty alt text and no controls.
  • Turn-by-turn directions: link to a full mapping service, and write the key steps as text.
  • A map as the only way to choose: when people search by name or postcode, put the list first.

Common failures

How it usually goes wrong

  • A map with no text alternative

    An image or a canvas map tells a screen reader nothing, and a tiny pin is hard to read when zoomed in. The list gives every fact the map shows.

  • Pins that are not buttons

    Markers drawn as divs or SVG paths cannot be reached with Tab and have no name. Each pin here is a button named by its store.

  • Pins in drawing order

    Tabbing through pins in the order a library placed them jumps around the city. Here they go nearest first, like the list.

  • Drag as the only way to move

    Dragging fails people who use a keyboard, a switch or a head pointer. Move buttons and keys do the same job.

  • Filters that update only the map

    If the list ignores the filters, the two views disagree and the text alternative is no longer equivalent. One filter updates both.

  • Scroll-wheel zoom on the page

    A map that zooms when the page scrolls over it traps people mid-page. Zoom here is buttons and keys only.

Notes

Building it

  • The details beside the map are copied from the store's list item when it is selected, so the map and the list can never say different things.
  • Pins are scaled by the inverse of the zoom, so they stay the same size and keep their 32 by 40 pixel targets at every zoom level.
  • One pin is in the Tab order at a time, the selected one, so the map is a single stop; the list below is the place to go through every store with Tab.
  • Without JavaScript the pins are not placed and the map is only a picture, but the list still holds every store.
  • Map libraries such as Leaflet and MapLibre have keyboard support of their own: check that markers are focusable buttons with names, and still provide the list.

Sources: W3C WAI: Complex images · Understanding WCAG 2.2: Dragging Movements · Understanding WCAG 2.2: Focus Not Obscured (Minimum)

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction