Map Rollovers

Let’s say you have a map of the world and you want to be able to highlight the country (or state, or county, etc.) as the cursor hovers over it or it is tapped.

Doable!

First, the map should probably be vector. SVG is almost certainly the right image format choice here. It will give us a crisp looking map at likely a reasonable size, and most importantly, give us the interactivity and easy styling we need.

Let’s use a state map of the United States as an example. Wikipedia has a perfect one for us to use.

Blank_US_Map_(states_only).svg.png

Let’s download that SVG and take a peek at the code:

Screen Shot 2017-01-15 at 10.38.03 AM.png

There’s some pretty well-formatted SVG here, with each state represented by a <path> with a unique ID.

The simplest possible map hovers would involve just dumping this SVG into the HTML, and adding a :hover to the CSS like:

path:hover {
  fill: red;
}

Screen Shot 2017-01-15 at 10.40.04 AM.png

But let’s take this to the next level.

Two-way hovers

By which I mean: you can hover over the state, or, you can hover over the name of the state in a list and you’ll see it highlighted.

Say we have a list of states like this:

<ul class="list-of-states">
  <li>Alabama</li>
  <li>Alaska</li>
  <li>Arizona</li>
  <li>Arkansas</li>
  ...

In the SVG we got from Wikipedia, the paths were like:

<path id="AK" fill="#D3D3D3" d="..." />

Unfortunately, there isn’t a simple, currently existing way to connect those elements programmatically. I created that connection by hand, using a data-* attribute for each state in the list:

<ul class="list-of-states">
  <li data-state="AL">Alabama</li>
  <li data-state="AK">Alaska</li>
  <li data-state="AZ">Arizona</li>
  <li data-state="AR">Arkansas</li>

Now we have what we need.

Let’s attach an event handler both from when the list items are hovered onto and hovered off. When those things happen, through the data attribute we’ll figure out which SVG state is relevant, then add classes to both.

var wordStates = document.querySelectorAll(".list-of-states li");

wordStates.forEach(function(el) {

  el.addEventListener("mouseenter", function() {
    var stateCode = el.getAttribute("data-state");
    var svgState = document.querySelector("#" + stateCode);
    el.classList.add("on");
    svgState.classList.add("on");
  });

  el.addEventListener("mouseleave", function() {
    var stateCode = el.getAttribute("data-state");
    var svgState = document.querySelector("#" + stateCode);
    el.classList.remove("on");
    svgState.classList.remove("on");
  });

});

Now we can use that class name to style each and every thing to our very wish.

.list-of-states li.on {
  background: red;
  color: white;
  font-weight: bold;
}
path.on {
  fill: red;
}

state-rollovers.gif

We can do the exact reverse thing for the state paths too, so that the state in the list highlights when we hover over the map:

var svgStates = document.querySelectorAll("#states > *");

svgStates.forEach(function(el) {

  el.addEventListener("mouseenter", function() {
    var stateId = el.getAttribute("id");
    var wordState = document.querySelector("[data-state='" + stateId + "']");
    el.classList.add("on");
    wordState.classList.add("on");
  });

  el.addEventListener("mouseleave", function() {
    var stateId = el.getAttribute("id");
    var wordState = document.querySelector("[data-state='" + stateId + "']");
    el.classList.remove("on");
    wordState.classList.remove("on");
  });

});

DRYing up

Unfortunately, that’s a lot of repetitive code. So let’s clean it up. We can abstract the functionality into:

  • Adding the “on” state (from hovering the map)
  • Adding the “on” state (from hovering the list)
  • Removing all “on” states

We can make those into functions which we can call as needed:

var wordStates = document.querySelectorAll(".list-of-states li");
var svgStates = document.querySelectorAll("#states > *");

function removeAllOn() {
  wordStates.forEach(function(el) {
    el.classList.remove("on");
  });
  svgStates.forEach(function(el) {
    el.classList.remove("on");
  });
}

function addOnFromState(el) {
  var stateCode = el.getAttribute("data-state");
  var svgState = document.querySelector("#" + stateCode);
  el.classList.add("on");
  svgState.classList.add("on");
}

function addOnFromList(el) {
  var stateId = el.getAttribute("id");
  var wordState = document.querySelector("[data-state='" + stateId + "']");
  el.classList.add("on");
  wordState.classList.add("on");
}

wordStates.forEach(function(el) {
  el.addEventListener("mouseenter", function() {
    addOnFromState(el);
  });
  el.addEventListener("mouseleave", function() {
     removeAllOn();
  });
});

svgStates.forEach(function(el) {
  el.addEventListener("mouseenter", function() {
    addOnFromList(el);
  });
  el.addEventListener("mouseleave", function() {
     removeAllOn();
  });
});

all-hover.gif

Mobile support

Have you heard? There isn’t a single cursor on (most) touch screens.

We can easily make it work with taps though, which was part of the reason for DRYing out. In addition to binding to mouseenter and mouseleave, we also do touchstart (click would work too).

wordStates.forEach(function(el) {
  // other vents

  el.addEventListener("touchstart", function() {
    removeAllOn();
    addOnFromList(el);
  });
});

svgStates.forEach(function(el) {
  // other events

  el.addEventListener("touchstart", function() {
    removeAllOn();
    addOnFromState(el);
  });
});

Demo

See the Pen Hoving States by Chris Coyier (@chriscoyier) on CodePen.

Comments

  • Andy Waldrop

    We did this about a year ago on medpost.com. Hardest part was making it work on older browsers as we still had to support ie8 :(

    We ended up building a zoom view to show metro markets and individual locations as well.

    • chriscoyier

      IE 8 would be pretty rough as there is no SVG support at all! This demo won’t work in IE 9 either, as it uses `classList`, but a few alternations should make the browser support pretty OK. Maybe slap some jQuery in there instead of doing things native.

  • Demo doesn’t work for me in Chrome 49 or Firefox 48. Is this something only more advanced browsers can see? :-(

    • chriscoyier

      Seems to for me. Are you seeing errors in console or anything?

      Chrome is currently at 56 and Firefox at 51, but I don’t think anything in here is so modern those versions matter much.

  • Sergio Forés Raga

    Really good staff!

  • Balubino

    Nice post.
    Confirm demo’s not working in FF 49: complaints about forEach on states names.

  • MasterCharlz

    Interesting to see that the DRY code is longer than the original code. Makes me feel way better as a newbie to Javascript.

  • agopshi

    @chriscoyier addOnFromList, addOnFromState, wordStates.forEach, svgStates.forEach. That’s not DRY. That’s WET. Literally. Write Everything Twice.

    This is DRY: http://codepen.io/agop/pen/xgXWWQ.

Related Articles