Chapter 4. Creating a Responsive SVG Sprite

The “scalable” part of SVG is perhaps the most powerful aspect of the graphics format. Using the viewBox attribute and our knowledge of shapes and paths, we can crop an SVG to any size on the fly, knowing that our intentions within the coordinate space will be preserved.

If we remove the width and height attributes from a common SVG, we’ll see something interesting. The SVG expands itself to the full width of the viewport, maintaining the aspect ratio of everything within the DOM.

If we use CSS keyframes or JavaScript to move SVG elements such as circle or path while scaling this SVG up or down, the increments that they will move will scale as well, along with the graphic. This means that if you scale a complex SVG using percentages, a flexbox, or other techniques, your animation will scale accordingly. You don’t have to adjust anything for mobile or other sizes; you can focus on writing the code correctly one time.

The completed animation is completely scalable. In the following CodePen example, you can randomly resize the animation while it’s running and watch it instantly adjust. This is very useful for responsive development. The animation in Figure 4-1 uses a completely fluid approach.

Figure 4-1. Different states of the same animation at different sizes

We design the whole thing first, and then slowly reveal things. Figure 4-2 is what our initial SVG (before we add any animation) looks like.

Figure 4-2. Original design in Illustrator—we design everything first, and then slowly reveal things

We could also design for responsiveness SVG in two other ways. In this chapter, we’ll do a deep dive into a technique that uses SVG sprites, similar to the ones we created in Chapter 3. This is easy to work with in CSS. In Chapter 15, we’ll cover a more advanced JavaScript approach as we hide, show, collapse, and rearrange our content.

SVG Sprites and CSS for Responsive Development

Joe Harrison has demonstrated a really nice way of collapsing SVG sprites for less visual information on mobile, shown in Figure 4-3. We’re going to use that to our advantage and create a similar, incrementally more complex sprite as we shift viewports from mobile to desktop.

Figure 4-3. Joe Harrison’s very impressive SVG logo sprites

As our screen sizes shrink and grow, the graphic follows suit and condenses or reveals visual complexity. It’s helpful to the user to not be served visually complex graphics on small displays, where too much information can become noise. Animations can be modified with the same considerations as the typography and layout, adjusting to the viewport and clarifying the design.

We’re going to be working with a responsive illuminated drop-capital letter to show how a standalone illustration can adjust (Figure 4-4). The design was inspired by the Book of Kells, an incredibly decorated medieval manuscript, to show how a standalone drawing can adjust to different viewport sizes. We start from this design, which we’ll use as our “map.” Other people plan differently, working in-browser or making sketches; choose whatever method makes you most productive.

Figure 4-4. Designing our “map”

Grouping and DRYing It Out

Now that we know how the final product appears, we can refactor the design to group like sections together, based on what’s most important for the associated viewport width. We can also simplify the design by identifying shapes used in both the first and second versions, keeping just one copy of each shared shape.

All of the elements are assigned semantic ID names such as “mountain” or “bridge.” The most detailed shapes also get a shared ID that we can progressively show for larger viewports. If the first illustration is kells1, the group particular to the second illustration is kells2, and the last is kells3.

In order to make the SVG scalable to shared container values, the last illustration becomes the same size as the first; SVG’s built-in responsiveness will take care of the rest.

We end up creating only two areas of the sprite sheet, with both having the same width so that we can scale the whole image at once (Figure 4-5). The top graphic is more complex; it holds information for both the tablet and desktop instances.

Figure 4-5. The sprite once we reduce repetition and get it ready for export

Once we have this view, we run it through SVGOMG, using the web-based GUI to check that there’s no distortion and toggling off the option to Clean IDs and also Collapse Useless Groups. We can later change the IDs to classes if we wish and clean up some of the cruft from the export. I do this by hand or with find and replace, but there are myriad ways to accomplish it.

The optimized SVG is placed inline in the HTML rather than included as a source URL background image like in the previous techniques. Now we can set areas to hide and show with a mobile-first implementation:

@media screen and ( min-width: 701px ) {
  .kells3, .kells2 {
    display: none;
  }
}

We can also adjust the animation parameters slightly, depending on the viewport, in order to refine the movement for each version:

[class^="mountain"], [class^="grass"] {
  ...
  transform: skew(1.5deg);
}

@media screen and ( min-width: 500px ) {
  [class^="mountain"], [class^="grass"] { 
    transform: skew(2deg);
  }
}

At this point the width and height are removed from the SVG and we can add in preserveAspectRatio="xMidYMid meet" (though that’s the default, so it’s not strictly necessary) to make the SVG fluid. With these alterations, it will adjust to the container size instead, which we set based on percentages (a. flexbox or any other responsive container would work here too):

.initial { 
  width: 50%;
  float: left;
  margin: 0 7% 0 0;
}

The viewBox Trick

There is one catch—even if we assign the bottom layer a class and hide it, there will be an empty gap where the viewBox still accounts for that space. In order to account for that area, we can change the viewBox in the SVG to show only the top portion:

viewBox="0 0 490 474"

That will work, but only for the two larger versions. The smallest version is now obscured, as the viewBox is providing a window into another portion of the SVG sprite sheet, so we will need to adjust it. This is akin to changing the background position in CSS to show different areas of a sprite sheet. But because we’re adjusting an SVG attribute, we will need JavaScript, as CSS doesn’t yet have that capability:

var shape = document.getElementById("svg");

// media query event handler
if (matchMedia) {
	var mq = window.matchMedia("(min-width: 500px)");
	mq.addListener(WidthChange);
	WidthChange(mq);
}
// media query change
function WidthChange(mq) {
  if (mq.matches) {
    shape.setAttribute("viewBox", "0 0 490 474");
  } else {
    shape.setAttribute("viewBox", "0 490 500 500");
  }
};
Note

There’s an ongoing discussion of adding these types of adjustments into the CSS spec on the W3C’s GitHub page; Jake Archibald has also raised the issue. If the proposal is adopted, you will be able to update all of the viewBox changes in media queries and keep presentation implementation in one language.

Now when we scroll the browser window horizontally the viewport will shift to display only the part of the SVG we want to expose. Our code is now primed and ready to animate.

Responsive Animation

When we export from a graphics editor, we have a unique ID for every different element. My preference for repeated elements is to use classes, so I did a find and replace of IDs to classes (Illustrator will still add some unique numbers to the names of each class, but we can target them using a CSS attributeStartsWith selector):

[class^="mountain"], [class^="grass"] {
  animation: slant 9s ease-in-out infinite both;
  transform: skew(2deg);
}

You’ll see here that we begin with a transform set on that element; this keeps the keyframe animation nice and concise. The animation will assume that the 0% keyframe corresponds to the initial state of the element; to create a very efficient loop, we can define only the changes halfway through the animation sequence:

@keyframes slant {
  50% { transform: skew(-2deg); }
}

Some elements, such as the dots and stars, share a common animation, so we can reuse the declaration, adjusting the timing and delay as needed. We use a negative offset for the delay because we want it to appear as though it’s running from the start, even though the element animations are staggered. Animation keyframes will use the default positioning set on the element as the 0% and 100% keyframes unless they are specified otherwise. We use this to our benefit to write the least code possible:

@keyframes blink {
  50% { opacity: 0; }
}

[class^="star"] {
  animation: blink 2s ease-in-out infinite both;
}

[class^="dot"] {
  animation: blink 5s -3s ease-in-out infinite both;
}

We also need to add a viewport <meta> tag, which gives us control over the page’s width and scaling on different devices. The most common one will do:

<meta name="viewport" content="width=device-width, initial-scale=1">

Get SVG Animations now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.