bp

SVG Icon Sprites in Eleventy

sprite sheet for a pixel-art game featuring a female mage character

So you want to put some SVG icons on your 11ty site, hey? This technique lets you include icons in your posts easily. With a little initial investment, adding and using icons should be easy for you and easy on your users.

Step 1: The Collection

Let's start by creating a collection for our icons. In a directory of your choosing (mine is called /icons), add an 11ty directory data file (e.g. icons.json), and include the following contents:

{
  "permalink": false
  "tags": [
    "icon"
  ]
}

The permalink: false setting prevents 11ty from writing the icons to the output dir, and the tag adds each one to the new icon collection.

Populate the /icons directory with your SVG files.

Step 2: The Shortcode

That having been accomplished, let's define the shortcode that we'll use to display icons on our pages. The shortcode will take a name and an optional map of HTML attributes, and works like this:

{% icon 'html5' %}
{% icon 'svg', 'aria-labelledby'='svg-w3c' %}
{% icon '11ty', title='eleventy' %}

By storing a set of icons requested on each page object, our sprite sheet will only render those icons as actually are needed, saving your readers' data. What's more, since we use use href to actually display the icons, we only need to render each SVG drawing once per page, further reducing page sizes when there are multiple instances of the same icon in use.

eleventyConfig.addShortcode('icon', function icon(name, kwargs) {
  this.ctx.page.icons ||= new Set();
  this.ctx.page.icons.add(name);
  const { __keywords, ...attrs } = kwargs ?? {}
  const attributes =
    Object.entries(attrs)
      .map(([name, value]) => `${name}="${value}"`)
      .join(' ');
  return `<svg ${attributes}><use href="#${name}-icon"></use></svg>`;
});

Abusing the page object like this kinda gives me the creeps, but 🤷, its works!

Step 2: The Sprite Sheet

With our icons and shortcodes up and running, we still can't see any icons on our pages, so let's add our sprite sheet to our base HTML. Put the following nunjucks snippet just before the `` tag of your most basic page template:

{% if page.icons %}
<svg id="icon-sprite-sheet">
  <defs>
    {% for icon in collections.icon %}
      {% if page.icons.has(icon.fileSlug) %}
        <g id="{{ icon.fileSlug }}-icon">{{ icon.content | safe }}</g>
      {% endif %}
    {% endfor %}
  </defs>
</svg>
{% endif %}

The if nunjucks tag ensures that only icons that had been requested on this page via shortcode actually get printed to the final HTML.

Let's now add some visually-hidden styles to our sprite sheet, so it won't take up any space on the page.

#icon-sprite-sheet {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

Accessibility Concerns

Make sure that each icon you use either has a <title> in the SVG, or use aria-label*, or remove the icon from the a11y tree.

This technique could be a nice little optimization for your pages, or could turn out to seriously cut down on your bytes-over-the-wire, depending on how you use icons.

If you have any ideas for improvements, let me know on mastodon.