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.
Let’s download that SVG and take a peek at the code:
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;
}
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;
}
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();
});
});
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.