Making Mapbox Popups Accessible

popups over a map

accessibility |

CSS |

HTML |

Javascript |

Mapbox |

Meteor |

Accessibility is no longer “nice to have” in web design. It must be a core part of your development. Here’s how we made Mapbox accessible. We worked on a project that locates farmers and farmers’ markets within a certain distance of the user. We’ve long been fans of Mapbox, so it seemed a natural fit for this project. Unfortunately, its map markers and popups aren’t keyboard or screen reader accessible. Luckily, the tool itself is highly extensible, so we were able to do a lot to remedy these issues.

Our goal is to allow users to navigate the map markers with their keyboards, open the associated popups, navigate through those, and then close the popups and be returned to the map. Let’s get started.

Keyboarding Surfing

The first issue we had was that a keyboard user simply tabs straight past a default mapbox setup, never focusing the markers. Luckily, this is an easy fix: all we need to do is ad the tabindex=0 property to each marker (you can learn more about tabindex at the excellent webaim.org).

That’s pretty easy to do in Mapbox. Following the tutorial for adding markers, we need to add two lines of code, setting tabindex to 0 (for tabbing about) and adding the aria role link (for screen readers):

el.setAttribute('tabindex',0);
el.setAttribute('role','link');

We’d also like screen readers to read out the name of the element, so let’s add a title based on the data we used to generate the markers:

el.setAttribute('title',provider.name);

With that in place, a user can tab into the map and focus each marker, as well as use their screen reader to navigate. Of course, that doesn’t do us much good without being able to actually open the popup. We need to make the enter key open these popups in addition to clicking on them. This is…a bit more complicated.

Enter the Popup

The first challenge is the event handlers. Because we’re attaching keypress to the markers and we’ve attached click to the map, we need a popup opening function that’s available to both. To accomplish that, we created a function available to both: openPin(target).

This function exactly mimics the function laid out in the popup tutorial. Because we need to access the map object in this function from outside of setup function, we declare map at the global scope. Now, instead of opening the popup in the map setup function, we use openPin and send it the target based on the click location. But what do we do about clicking enter? Well, it’s similar, honestly—we add the following at the end of the function that creates our popups:

// Open popups with enter key
$('.mapboxgl-marker').keypress(function markerKeypress(event) {
  const keycode = event.which;
  const marker = $(this)
  if (keycode === 13 /* enter key */) {
    openPin(this);
  }
});

That works pretty well! However, we’ve just created an element on the page that’s approximately 65,535 tab-keys away. What we’d really like to do is focus this popup. To accomplish this, our popup has a div in it called .info to which we’ll add tabindex=0. Then we focus that element.

While we’re at it, there’s another small issue—we only want one popup open at a time. So we’ll remove all the other ones when we open this one.

Here’s what the code looks like with that update:

// Open popups with enter key
$('.mapboxgl-marker').keypress(function markerKeypress(event) {
  const keycode = event.which;
  const marker = $(this)
  if ($('.mapboxgl-popup').length !== 0) {
    $('.mapboxgl-popup').remove();
  }
  if (keycode === 13 /* enter key */) {
    openPin(this);
  }
  $('.info').focus()
});

OK, so now we successfully open a popup with the enter key and focus it when it opens. Let’s talk about the popup itself!

Popup ’n’ Lock

Now that we’ve got the popup focused, we want to make sure users don’t accidentally tab off of it and find themselves way far away from the map. To accomplish that, we’re going to add a div at the end of the popup that looks like this:

<div class="tab_loop_end" tabindex="0"></div>

Next we’re going to add the tab_loop_start class to the .info element from earlier. We hide the tab_loop_end div using CSS so that it’s only relevant to keyboard users. Here’s the CSS we use to hide elements visually:

.hidden {
    position: absolute;
    left: -10000px;
    top: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

Now we’re going to use some javascript to loop when that element gets focus. We’ll insert this Javascript in the openPin() function.

$(.tab_loop_end).focus(function() {
  $(this).closest('.tab_loop_start').focus();
});

This keeps the user inside the element until they close it. As it stands, we’re looping them such that they never focus the close icon. That means the user is trapped forever, which isn’t going to make us any friends. Let’s fix that.

Closing Time

We’re going to give the user two ways to close the popup: by hitting the escape key, and by clicking a new, hidden close link. To capture escape keys, we’re going to add the following to the openPin() function:

$('body').keydown(function escapeBody(event) {
  const keycode = event.which;
  if (keycode === 27 && $('.mapboxgl-popup').length !== 0) {
    // $('.mapboxgl-popup-close-button').click();
    popup.remove();
    $('body').off('keydown');
    console.log('popup removed');
  }
});

Note that we use keydown here because keypress won’t catch the escape key. We can get away with $('body').off('keydown'); here because this is the only keydown event listener we put on the body, but you might need to be a little more careful depending on your site, application, framework, or what have you.

As for the hidden close link, it’s pretty easy to do.

Here’s the HTML we use:

<a id="hidden_close_link" href="#" title="close popup">close</a>

And here’s the CSS for the element:

#hidden_close_link {
  opacity: 0;
  position: absolute;
  right: .1rem;
  bottom: .1rem;
} 

#hidden_close_link:focus {
  opacity: 1
}

As for the javascript:

$('#hidden_close_link').focus(function() {    
  event.preventDefault();
  const keycode = event.which;
  if (keycode === 13 /* enter key */) {
    $('.mapboxgl-popup').remove();
  }
}

There! Now people can close the popup with their keyboard. But what happens when they do? Their keyboard focus ends up…somewhere. We’d like to make sure they end up back where they started.

Closer

We’re lucky here that Mapbox provides close as an event on popups. We need to make a function that figures out what element we should focus on. In our case, we have a data-id property on the popups and the markers (derived from the id property of the element), so we use that to make the determination. Here’s what we added to the popup creation in openPin() (right before .addTo()):

.on('close', function(){ 
  if ($('.mapboxgl-popup').length === 0) {
    $(`.mapboxgl-marker[data-id=${id}]`).focus();
  }
})

With that in place, users will be focused back to the pin they had open previously. It happens for mouse users as well, which doesn’t hurt anything.

That’s All, Folks

Share and enjoy!