The hippo exhibit in the St. Louis Zoo contains a pool of water bordered by a glass wall. Watching the hippos move underwater reveals why hippos are named after a Greek word meaning ‘river horse’. Their movements are much like the galloping of a horse as they push off the ground to propel themselves around underwater.
In this post, we’ll walk through the basics of setting up this interactive SVG element using CSS, JS and HTML. Specifically, we’ll explore:
- Creating a custom SVG in Adobe Illustrator
- Working with classes in object-oriented JS
- Manipulating DOM elements within a single page application
In some of the examples, you’ll see classes prefixed with a “js-”. To help decouple my CSS styling and JS actions, I added this prefix to identify classes targeted by JS. You can check out the full frontend and backend repos for more context on GitHub.
Creating the SVG
If your project requires a custom map or other SVG element, you can use a vector graphic editing program like Adobe Illustrator to create your image. I based my graphic on public domain map data of the zoo made available by © OpenStreetMap contributors.
When creating your own SVG in Illustrator, make layers your friend. When you export your image as an SVG, each layer gets wrapped in group tags,
<g></g>. Each group also includes an id attribute equal to the name you set for the layer. This will help with targeting elements with JS later. For the zoo map, I set up a new layer for each zone so each map zone could be targeted and styled separately.
After exporting your file as an SVG, you can open it up in a code editor for fine tuning. If the SVG you’re working with doesn’t have line breaks or indentation, adding those can help you understand the structure of the graphic and what each
<g> group contains.
Here’s what the beginning of the zoo map SVG looked like with spacing adjustments.
NOTE: The paths in the code samples below have been shortened with ellipses to save space.
In the code above, you’ll notice a few things. First, our layer name is showing up as an id inside our first group. The group contains two paths, one representing the zone shape and the second representing the animal icon it contains.
You’ll also notice that each path was generated with a class. These classes are referenced in the
<style> tag at the top, which assigns the initial colors for the SVG paths. We can target these default classes with CSS and JS, or we can create our own more meaningfully-named classes as shown below. We can also add
<title> tags with a short description for each group to aid accessibility.
Our updated SVG:
How you set up ids and classes can depend on what elements you want to style or target together. There’s no one right way, so find an approach that makes sense for your context.
One thing you might notice is that some groups contain a
<polygon> tag instead of only
<path> tags. Why? When we exported our SVG, the polygon tag was generated for elements composed only of straight lines, while the path tag was generated for elements that contain curves.
Making the Map Interactive
Now that we have our map created, let’s make it interactive with the following features:
- When a user hovers over a zone, add a thicker border and make the zone color darker
- When a user clicks a zone to select it, let that ‘active’ zone stay colored while the other ‘inactive’ zones become gray
- When a zone is selected, display that zone’s information beside the map and its exhibits underneath
There are a variety of ways to achieve these goals, but we’ll walk through an approach using object-oriented JS and classes. We’ll start by setting up three JS classes: Zone, Exhibit and MapZone.
If you haven’t encountered JS classes before, it’s helpful to know that the keyword
this when used inside a class refers to the specific instance of the class we’re creating/working with.
Class 1: Zone
When the app first loads, our
Zone class is used to create a zone object for each of our six zones using data about the zone fetched from a Rails API backend. Each zone object contains properties such as the zone’s name, unique database id, description, and its related map zone element. We can reference the value of these properties in the code with dot notation,
zone_1.description, or bracket notation,
This class is separate from the
MapZone class, which focuses on the behavior and properties of the SVG map zones.
Setting up the Zone class:
Since we set custom class names on the map elements based on the zone names, we can use these classes to find each zone’s matching map element.
Notice that we set the database id we received from the API as a data attribute on the zone’s related map element. This will come into play when we set up our map zone objects next.
Class 2: MapZone
When the app starts, six instances of the
MapZone class are also created. The
MapZone constructor sets properties for each map zone object, such as the group element and the specific SVG path/polygon the object represents. Having an easy way to access this path element separate from the map group as a whole will help us change the zone’s fill color without changing the color of the animal icon also contained in the map group.
Setting up our MapZone class:
In our constructor, we’ll also add a property,
zoneId, and set it equal to the id of its related zone object. Remember that we set a data attribute,
id, on each map zone path when we created our
Zone instances. This id comes from the
Zone table of the database we’re accessing through the backend API. Each zone record in the database has a unique id value, which makes it a good attribute to use when we want to reference a specific zone. Since each map path has a data attribute equal to the id of the zone it represents, we can pull this data attribute id value and use it to identify the specific zone object we want information from when the map section is clicked.
Each new map zone is pushed into a static array,
all, which we’ll use when setting up our
handleClick method. Note that we added event listeners for click, mouse-in and mouse-out events on each map group. These will come into play coming up.
Class 3: Exhibit
Let’s take a look at the
Exhibit class next. Since there are a limited number of exhibits, I decided to send a fetch request to the backend API when the app starts. However, you could set this up to only send a fetch request when a map zone is clicked instead of preloading all the related content for your map.
Setting up our Exhibit class:
Each exhibit will be rendered as a card, so we’ll add an
element property to every exhibit object to represent that card
div. We’ll add our related CSS card class to this
div. Next we attach an event listener to each card that will allow the card to toggle between displaying the main exhibit info (front) and a fact about the exhibit’s species (back) with each click.
We’ll also make sure to push each exhibit into our empty
Exhibit.all array so when a map zone is clicked, we can filter the array and only select exhibits that have a matching zone id property.
Handling Clicks and Hovering
Now that our central classes are set, we need to listen for events on the map, specifically clicks and hover states, so we can apply different styles based on user interaction. While CSS allows us to change styling based on hover state, I decided to use JS to handle both clicks and hovering effects since they both relate to map interaction.
MapZone constructor, we set three event listeners on each individual map group. This allows us to trigger a specific action, or callback function, when a specific event type occurs.
We’ll start by handling hover states, so let’s set up
handleMouseLeave functions in our
MapZone class as instance methods. Thinking back to our goal, we want to darken the color of a zone when a user moves the mouse over it (
mouseenter event) and return it to its original color when the user moves the mouse out (
We can achieve this by adding a class,
js-hover, to the map path being hovered over that we can style in the desired way with CSS.
Our next goal is to change zone colors based on click events, setting inactive/non-selected zones to gray, with a darker gray color on hover, and active/selected zones to their original color without any other effects. To do this, we can add an additional class that indicates whether a zone is ‘active’ or ‘inactive’ based on clicks. Once we set up these additional classes, we can combine the class selectors in our CSS to create styles for all possible interaction states (
.js-hover.js-active — user hovers over active zone,
.js-hover.js-inactive — user hovers over inactive zone, etc.).
We’ll set up our click-triggered features in a
handleClick method inside our
MapZone class so that all instances of the class (i.e., each map zone) will have access to it.
Setting up our handleClick method:
Let’s walk through this
handleClick method, which gets triggered each time a map zone in the SVG is clicked.
First we toggle whether a map zone has an
inactive class based on which map zone has been clicked. These classes will apply our chosen styling effects that change the color and hover styles of the map zones.
Next we find the zone object from our
Zone class that contains the information about the selected zone we want to display beside the map. We’ll call a method,
attachToDom(), on this zone object. This method will build out the zone element we want to add to the DOM and display it beside the map.
Our exhibits container and exhibits card container are present on the DOM from the start of the app, but they aren’t yet visible on the page. Their display style is initially set to
‘none’, so to make them visible we need to change that to
‘block’. We also need to make sure we have a fresh slate inside our exhibit card container with each new click so we don’t display any exhibits that might have been added to the DOM with a previous click.
Now we’re ready to filter our
Exhibit.all array that holds all of our zoo exhibits and find the exhibits that match the selected zone and attach each one to the DOM.
Since we’re adding to the length of our page when we add the exhibit cards, we’ll also make a button visible that allows users to scroll back up to the top with one click.
Targeting SVG elements with JS can be a fun way to add interaction to web applications. We’ve seen how layers can help keep custom SVGs organized and we can add JS event listeners to different paths within an SVG. JS opens up endless options for adding engaging elements to your apps, so let the inspiration run wild. 🦛