The quest to find an easy to use, extensible, and performant spriting strategy has been long. In the beginning, there was the old school sprite sheet that a designer would generate and pass off to a web developer. This was pretty finicky and it was often quite a chore to add something new to the sheet. The developer would have to hope that nothing unexpectedly shifted by a pixel or two.
Skip forward to 2012 and Font Awesome arrived on the block, which expertly checks off the easy to use box. However the extensibility and performance aspects of the solution leave some room for improvement.
- Even an optimized custom build still results in loading the CSS library and a number of web fonts.
- Sub-setting a custom build does not seem to support custom icons.
- Adding custom icons to a full build requires a pro-level subscription, enabling beta features, and operating within the Font Awesome walled garden.
- Using fonts makes it difficult to do some really neat and advanced stuff.
A modern and performant strategy
Essentially, the approach that I've taken with sprites involves leveraging automation, SVG documents, and some very simple theme scaffolding. For the automation component, I use the SVG Sprite utility coupled with a Gulp wrapper plugin.
var gulp = require('gulp'); var svgSprite = require('gulp-svg-sprite'); const sprites = function(done) { gulp.src('/src/sprites/*.svg') .pipe(svgSprite({ svg: { namespaceIDs: false, namespaceClassnames: false, xmlDeclaration: false, doctypeDeclaration: false, }, mode: { defs: { inline: true, }, } })) .pipe(gulp.dest('/dist')); done(); }; gulp.task('build', gulp.parallel(sprites, css, js));
This process takes all SVG assets from the source directory and compiles them into a single file. This file takes on the form of:
<svg style="display:none"> <defs> <svg id="svg-a">...</svg> <svg id="svg-b">...</svg> <svg id="svg-c">...</svg> </defs> </svg>
This file is then injected into the DOM response of every page that they are to be used on.
{# Somewhere at the bottom of html.html.twig #} {# This file is inherently trusted and generated by trusted users #} {{ source('/themes/custom/my_theme/dist/sprite.defs.svg')|raw }}
Next, a utility twig file was created to simplify the placement of sprite references.
{# sprite.html.twig Variables: name - the name of the sprite to reference #} {% set sprites = { 'svg-a': '0 0 448 448', 'svg-b': '0 0 512 512', 'svg-c': '0 0 512 512', } %} {% if name in sprites %} {{ attach_library('my_theme/sprite') }} <svg class="sprite" viewBox="{{ viewboxes[name] }}"> <use xlink:href="#{{ name }}" /> </svg> {% endif %}
Of course, there has to be a little bit of CSS that sets some sane default styles -- most importantly, setting a height that makes the sprite look good wherever it might appear in a document.
# my_theme.libraries.yml sprite: version: 1.0.0 css: theme: dist/sprite.css
/* sprite.css */ .sprite { height: 1em; width: auto; vertical-align: middle; }
Finally, sprites can now be placed anywhere in twig.
{# some-random-file.html.twig #} {% include '@my_theme/sprite.html.twig' with { name: 'svg-a' } %}
A note on security
As a general rule, SVGs are fairly dangerous in nature. This article assumes that all SVG files are reviewed for safety in order to prevent cross site scripting problems. There is no additional layer of security presented here that will automatically help.
A note on performance
This technique delivers all sprites inline within the DOM response. This eliminates the need for a round-trip back to fetch additional resources. In the case of font awesome, it can eliminate two round-trips: one to fetch CSS and another to fetch the font resources themselves.
Taking things further
There are some advanced techniques that can be used to really take things to the next level. A future post may cover some of these topics including advanced transition effects, conditional symbol rendering, multi-tone sprites, and how to compose new and exciting sprite variations from individual symbols with Javascript.