Nested <use> inside SVG <symbol>
Nested <use>
elements inside of SVG <symbol>
elements do not seem to work in any browser that I have at hand. The only solution that I could think of was inlining the use elements with cheerio. That solution is admittedly awkward but at least it works.
Creating A Flag Icon Set
The problem occured in a project where I needed a flag icon set. I decided to give SVG sprites with <symbol>
elements a try. So let's get that up and running with npm and gulp.
$ mkdir nested-use
$ cd nested-use
$ npm init --force
npm WARN using --force I sure hope you know what you are doing.
Wrote to .../package.json:
...
Next we have to install the dependencies:
$ npm install --save-dev gulp
...
$ npm install --save-dev gulp-svgmin
...
$ npm install --save-dev gulp-svgstore
...
$ npm install --save-dev flag-icon-css
...
$ npm install --save-dev gulp-rename
...
flag-icon-css is a collection of country flags. They are stored in individual SVG files. The first step was to combine them into one large SVG file. The following gulpfile.js
will do the job:
var gulp = require('gulp');
var svgmin = require('gulp-svgmin');
var svgstore = require('gulp-svgstore');
var rename = require('gulp-rename');
gulp.task('default', ['flags']);
gulp.task('flags', function() {
return gulp.src('node_modules/flag-icon-css/flags/4x3/*.svg')
.pipe(svgmin())
.pipe(svgstore())
.pipe(rename('flags.svg'))
.pipe(gulp.dest('.'));
});
This is all standard for SVG icon sets, so I will only briefly explain it. The flags
task is defined in line 8. In line 9 we fill the stream with the individual icon files from flag-icon-css
. The SVGs are then optimized in line 10, and combined into one large SVG document in line 12, the output filename is set to flags.svg
and it is finally written to disk with gulp.dest()
.
Execute the gulpfile with node node_modules/gulp/bin/gulp.js
or just gulp
if you have installed it globally.
Now load the resulting SVG file flags.svg
into your browser. You may be surprised that you do not see anything at all. This becomes clearer, when you look at the structure of the generated SVG:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>...</defs>
<symbol id="ad">...</symbol>
<symbol id="ae">...</symbol>
<symbol id="af">...</symbol>
<symbol id="ag">...</symbol>
<symbol id="ai">...</symbol>
...
</svg>
The SVG file is minimized. So you can best understand the structure by inspecting it in your browsers developer console.
The combined SVG contains a <defs>
element that contains <clipPath>
s for all icons, followed by a bunch of <symbol>
elements, one for each input file. A <symbol>
is much like a <g>
group, only that it is not rendered. That explains the white browser window you are looking at.
In order to see something we have to create a minimal html file:
<!DOCTYPE html>
<html>
<body>
<svg viewBox="0 0 640 480" width="160" height="120"
style="border: 1px solid #eeeeee;">
<use xlink:href="flags.svg#bg"/>
</svg>
<svg viewBox="0 0 640 480" width="160" height="120"
style="border: 1px solid #eeeeee;">
<use xlink:href="flags.svg#de"/>
</svg>
<svg viewBox="0 0 640 480" width="160" height="120"
style="border: 1px solid #eeeeee;">
<use xlink:href="flags.svg#in"/>
</svg>
</body>
</html>
What do we have here? We display three flags out of the collection, the Bulgarian, the German, and the Indian one. For each of them we create an inline <svg>
element with some minimal CSS that only consists of one single element, a <use>
element. The <use>
element references the shapes with the ids bg
, de
, and in
from the external file flags.svg
that we have just created.
In the browser you can see that this works perfectly for the Bulgarian and the German flag. But the Indian flag is just a black rectangle.
Note: Google Chrome does not display any SVGs when loaded from a file:///
URI. Fire up an ad hoc web server and view it there!
Structure Of the Broken SVG
The problem with the Indian flag (and a couple of other flags from that collection) is that it uses <use>
elements itself:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="480" width="640" version="1">
<path fill="#f93" d="M0 0h640v160H0z"/>
<path fill="#fff" d="M0 160h640v160H0z"/>
<path fill="#128807" d="M0 320h640v160H0z"/>
<g transform="matrix(3.2 0 0 3.2 320 240)">
<circle r="20" fill="#008"/>
<circle r="17.5" fill="#fff"/>
<circle r="3.5" fill="#008"/>
<g id="d">
<g id="c">
<g id="b">
<g id="a" fill="#008">
<circle r=".875" transform="rotate(7.5 -8.75 133.5)"/>
<path d="M0 17.5L.6 7 0 2l-.6 5L0 17.5z"/>
</g>
<use height="100%" width="100%" xlink:href="#a" transform="rotate(15)"/>
</g>
<use height="100%" width="100%" xlink:href="#b" transform="rotate(30)"/>
</g>
<use height="100%" width="100%" xlink:href="#c" transform="rotate(60)"/>
</g>
<use height="100%" width="100%" xlink:href="#d" transform="rotate(120)"/>
<use height="100%" width="100%" xlink:href="#d" transform="rotate(-120)"/>
</g>
</svg>
The author of that SVG has found a very clever way of putting together the Ashoka Chakra displayed on the flag of India. Unfortunately the inner <use>
elements obviously outsmart the browsers I have tested (notably Firefox 47, Chrome 51 and Safari 9.1.1, all on Mac OS X).
Inlining the <use>
Elements
An SVG <use>
element is just a placeholder for the element it references. So it should be possible to remedy the situation by inlining the <use>
s, that is by replacing them with the element that they reference.
We already use gulp-svgmin for optimizing the SVG graphics with SVGO. But SVGO does not have a plug-in that inlines <use>
elements which is kind of understandable because it does not minimize the SVG but rather does the opposite.
So I wrote the transformation myself with cheerio. First install "cheerio" as a dev dependency:
$ npm install --save-dev gulp-cheerio
And now edit gulpfile.js
as follows:
const gulp = require('gulp');
const svgmin = require('gulp-svgmin');
const svgstore = require('gulp-svgstore');
const rename = require('gulp-rename');
const cheerio = require('gulp-cheerio');
gulp.task('default', ['flags']);
gulp.task('flags', function() {
function inlineUse($) {
$('use').each(function(i, elem) {
var ref = $(this).attr('xlink:href');
if (ref !== undefined && $(ref) !== null) {
var copy = $(ref).clone(),
attributes = $(this).attr();
copy.attr('id', null);
for (var attr in attributes) {
if (attributes.hasOwnProperty(attr)
&& attr !== 'xlink:href') {
var value = attributes[attr];
if (attr === 'transform'
&& copy.attr(attr) !== undefined) {
value = copy.attr(attr) + ' ' + value;
}
copy.attr(attr, value);
}
}
$(this).replaceWith(copy);
} else {
$(this).remove();
}
})
}
return gulp.src('node_modules/flag-icon-css/flags/4x3/*.svg')
.pipe(svgmin())
.pipe(cheerio({
run: inlineUse,
parserOptions: { xmlMode: true }
}))
.pipe(svgstore())
.pipe(rename('flags.svg'))
.pipe(gulp.dest('.'));
});
What is new? Of course, gulp-cheerio
has to be required (line 5). It is invoked just inbetween svgmin()
and svgstore()
in lines 38 to 41. All the heavy lifting is done by the function inlineUse()
in line 10.
The function iterates over all <use>
elements in line 11. For all of them that have an xref:href
attribute that points to an existing element (line 13) a deep copy is created. You could optimize that by creating a shallow copy instead.
Because the referenced element gets duplicated, a possible id
attribute is deleted in line 17. This can possibly cause problems if the id is used elsewhere but this is quite unlikely.
The loop beginning at line 18 copies all attributes of the <use>
element except for the xlink:href
attribute into the clone of the referenced element. Only transform
attribute are chained with the transform
attribute of the cloned element.
Finally, the <use>
element is replaced with the modified copy (line 29). If the <use>
element was found to be invalid, it is simply removed from the DOM (line 31) because it seems that the sheer presence of a <use>
element prevents the browser from displaying the <symbol>
.
Run the gulpfile again with node node_modules/gulp/bin/gulp.js
and reload the HTML file in your browser. You should now see the Indian flag with the Ashoka Chakra in all its beauty.
Conclusion
Since I am not an SVG expert it is well possible that there is a much simpler solution to the problem. I am also not sure whether the browser behavior observed by me is a bug (that may be fixed in the future) or has a good reason. If you know more about it than me, please share your knowledge in the comment section below!
Leave a comment