Better SVG Sprite Re-use with Twig in Drupal 8

SVG is great for performance, flexibility, and they look great on high-resolution displays. This article explains how to make them easier to reuse.

There are many advantages to using SVG for the icons on a website. If you’re not familiar with SVG (aka Scalable Vector Graphics) it is an open-standard image format that uses XML to describe graphical images. SVG is great for performance, flexibility, and they look great on high-resolution displays. Though SVG can be really useful, one downside to implementing them can be the effort it takes to reuse them across a Drupal theme in various .html.twig files. SVG markup can be complex due to the number of attributes and, in some cases, there can also be a lot of markup. This issue can in part be mitigated by using inline SVG with the <use> element coupled with SVG Sprites. The combination of these two is a great solution—but unfortunately only gets part of the way to ideal reusability as the markup is still verbose for a single icon, and the number of SVG attributes needed can be hard to remember.

Wouldn’t it be nice if there was a way not to repeat all this code over again wherever we wanted to use an SVG? In this article we outline a way to reuse icons across template files in a Drupal 8 theme by creating a Twig helper function to reuse the markup.

Implementing a custom Twig custom function is fairly accessible to those comfortable with writing custom Drupal themes, but because of the way Drupal separates functionality between what can be defined in a module and what can be defined in a theme, we will need to create a module to contain our custom Twig function. To get started, create a new directory inside <drupalroot>/modules/custom with the following files and directories inside it. For the sake of this article I named the module svgy and the source is available on GitHub.

svgy
├── src
│   └── TwigExtension
│       └── Svgy.php
├── svgy.info.yml
└── svgy.services.yml

One important thing to note here is that we do not actually have a .module file in our directory. This is not required by Drupal, and the Twig extension which adds our function will be defined as a service and autoload the Svgy.php within the src/TwigExtension directory. Don’t worry if you are unfamiliar with defining custom services in Drupal 8, this article explains all you need to know to get going with what we need for our Twig function.

Now that the directory and file structure is in place, we first need to add the correct metadata to svgy.info.yml:

name: svgy
type: module
description: Add a Twig function to make using inline SVG easier.
core: 8.x

Next the necessary information needs to be added to svgy.services.yml. The contents of this file tells Drupal to autoload the Svgy.php file in the src/TwigExtension directory:

services:
  svgy.twig.extension:
    class: Drupal\svgy\TwigExtension\Svgy
    tags:
      - { name: twig.extension }

Now that Svgy.php is going to be loaded, add the code for our Twig function into it:

<?php

namespace Drupal\svgy\TwigExtension;

class Svgy extends \Twig_Extension {

 /**
  * List the custom Twig fuctions.
  *
  * @return array
  */
  public function getFunctions() {
    return [
      // Defines a new 'icon' function.
      new \Twig_SimpleFunction('icon', array($this, 'getInlineSvg')),
    ];
  }


  /**
   * Get the name of the service listed in svgy.services.yml
   *
   * @return string
   */
  public function getName() {
    return "svgy.twig.extension";
  }

  /**
   * Callback for the icon() Twig function.
   *
   * @return array
   */
  public static function getInlineSvg($name, $title) {
    return [
      '#type' => 'inline_template',
      '#template' => '<span class="icon__wrapper"><svg class="icon icon--{{ name }}" role="img" title="{{ title }}" xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="#{{ name }}"></use></svg></span>',
      '#context' => [
        'title' => $title,
        'name' => $name,
      ],
    ];
  }
}

More information regarding defining custom Twig extensions is available here. For the purpose of this article the most important part to explain is the inlineSvgMarkup. This function takes two arguments:

  • $name = the unique #id of the icon
  • $title = a string used as the title of the element for better accessibility

When invoked, this function returns a render array with the #type as an inline_template. We use this template type for two reasons: Twig in Drupal 8 has built-in filtering for security reasons and using inline_template allows us to get around that more easily. Since this markup is fairly small and not going to be changed often, we don’t need to create an extra .html.twig file to contain our SVG code.

Implementing the Custom Function In A Theme

Now that the custom twig extension for our icon function is created, how is this implemented in a theme? An example of the following implemented in a theme can be found on GitHub.

The first thing you have to do is add an inline SVG to your theme. The icons inside this SVG should have unique ids. In most situations it is best to generate this SVG “sprite” using something like svgstore as part of your build process. But to keep today’s example simple, I’ve created a simple SVG with two icons and placed it in a theme at <themeroot>/images/icons.svg.

After the icons.svg is in place in the theme you can include it in in the rendered page with the following in the themename.theme file:

function <theme_name>_preprocess_html(&$variables) {
  // Get the contents of the SVG sprite.
  $icons = file_get_contents(\Drupal::theme()->getActiveTheme()->getPath() . '/images/icons.svg');

  // Add a new render array to page_bottom so the icons
  // get added to the page.
  $variables['page_bottom']['icons'] = array(
    '#type' => 'inline_template',
    '#template' => '<span class="hidden">' . $icons . '</span>',
  );
}

This appends a new render array to the page_bottom variable that is printed out inside Drupal core’s html.html.twig template file. If you haven’t overridden this template in your theme, the icons will get printed out automatically. If you have overridden html.html.twig in your theme—just make sure you are still printing page_bottom inside it: ({{ page_bottom }} is all you need.

The .hidden class that is used in the wrapping <span> here is one provided by Drupal 8 core. It applies display: none; to the element. This results in hiding it both visually and from screen readers. Since the individual icons from the SVG will be referenced elsewhere in the page this is the desired outcome.

In the example theme on GitHub, this results in the following output on the page:

  <span class="hidden"><?xml version="1.0" encoding="UTF-8"?>
  <svg viewBox="0 0 130 117" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <polygon id="down-arrow" fill="#50E3C2" points="64.5 117 0 0 129 0"></polygon>
    <polygon id="up-arrow" fill="#50E3C2" points="65.5 0 130 117 1 117"></polygon>
  </svg></span>

Now that the SVG “sprite” is included on the page we can start referencing the individual icons within our SVG sprite with the custom Twig function. After you have cleared Drupal’s cache registry, icons can be added with this:

{{ icon('up-arrow', 'Navigate up') }}

The first argument passed into the function here, up-arrow, is used to reference an existing id in the SVG example included above. The second argument, Navigate up the page, is used as the title of the SVG to better describe contents of the element to users navigating with screen readers.

The resulting markup of this implementation of the icon() function when rendered in a page looks like:

  <span class="icon__wrapper">
    <svg class="icon icon--up-arrow" role="img" title="Navigate up" xmlns:xlink="http://www.w3.org/1999/xlink">
      <use xlink:href="#up-arrow"></use>
    </svg>
  </span>

As you can see, the resulting markup is much larger than just the single line where we used the icon() Twig function. This makes it a lot cleaner to reuse icons throughout our .html.twig files. In addition to making templates easier to read and not having to remember all the necessary <svg> attributes, we also ensure consistency in how icons get included throughout the project.

As Drupal themers, it is great to have flexibility like this provided by Twig. If you’re interested in digging deeper into other types of Twig extensions, I encourage you to checkout the Twig Tweak module, which has examples of other helpful Twig extensions.

Have more questions about SVG? Here are some other helpful articles

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!