I want to build an icon set with SVG and use it for buttons. These icons change their styles based on CSS classes applied. E.g. imagine a burger menu icon which transforms to a close icon when the menu is opened.
I’ve simplified the paths in all examples here for better readability. Please just imagine that those 2 paths would expand to finally show a cross, animated with CSS transitions. The expected result for the code here looks like this:
Use icons from .svg file
Of course I’d prefer using an external sprite SVG for caching like
<!-- icons-file.svg -->
<svg>
<def>
<symbol id="menu" viewBox="0 0 100 100">
<path d="M20,40H90"/>
<path d="M20,60H70"/>
</symbol>
<!-- more symbols to come -->
<style>
#menu > path {
/* ... */
stroke-dasharray: 40 500;
}
.active #menu > path:nth-child(1) {
stroke-dasharray: 80 500;
}
.active #menu > path:nth-child(2) {
stroke-dasharray: 50 500;
stroke-dashoffset: -10;
}
</style>
</def>
</svg>
and use it in my HTML with
<a href="#"><svg><use href="icons-file.svg#menu"/></svg></a>
This won’t even work halfway because <style/>
tags are ignored by <use/>
.
Add the sprite SVG inline
Thankfully I’m working on a single page PWA. Of course cashing is better, but with having the SVG inline I (hopefully) only add to the initial loading of the app, not every page/view.
<use/>
restrictions still remain, but here I can define my SVG styles in the HTML layer:
<html>
<!-- ... -->
<body>
<svg>
<def>
<symbol id="menu" viewBox="0 0 100 100"><!-- ... --></symbol>
<!-- more symbols to come -->
</def>
</svg>
<style>
#menu > path {
/* ... */
stroke-dasharray: 40 500;
}
.active #menu > path:nth-child(1) {
stroke-dasharray: 80 500;
}
/* ... */
</style>
<a href="#"><svg><use href="#menu"/></svg></a>
</body>
</html>
Now, the icon is displayed correctly in it’s default state (as a burger menu icon). But no matter where I apply the active
CSS class, even on the <use/>
node, it will never be respected. As far as I understand this is because CSS selectors can’t break the borders of shadow DOM.
But wait, what about the :host
selector? Having a rule like the following should do the trick:
:host(.active) #menu > path:nth-child(1) {
stroke-dasharray: 80 500;
}
/* or maybe */
:host(.active) path:nth-child(1) {
stroke-dasharray: 80 500;
}
Again, no luck. See an example on codepen. To be honest, I don’t know the reason. I guess it’s because the shadow DOM of <use/>
is closed, but could not find any information if that’s really the reason. This example on jsfiddle with a custom element and open shadow DOM works perfectly.
The CSS property: inherit
trick
One might say I should do something like this:
path {
stroke-dasharray: inherit;
}
Then I could set stroke-dasharray
to any value outside, e.g.
<a style="stroke-dasharray: 80 500"><svg><!-- ... --></svg></a>
Yeah, that were possible if I’d only have one path or all paths had the same length, starting and ending style. That’s not the case.
Whole SVG for each icon and every instance
Finally, my only way out right now is to have the icon’s SVG directly in the anchor:
<a href="#">
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M20,40H80V20"/>
<path d="M20,60H70V70"/>
<style>
path {
fill: none;
stroke: black;
stroke-width: 6px;
transition: all 0.5s;
}
path:nth-child(1) {
stroke-dasharray: 40 500;
}
.active path:nth-child(1) {
stroke-dasharray: 80 500;
}
path:nth-child(2) {
stroke-dasharray: 10 500;
}
.active path:nth-child(2) {
stroke-dasharray: 50 500;
stroke-dashoffset: -10;
}
</style>
</svg>
</a>
This is the worst case. No caching and for icons I use in lists, I blow the result heavily by having the same code dozens of times.
So my question is: Do I oversee something? Is there a better way?
2
Answers
You cannot write selectors that cross into a shadow DOM, but you can inherit property values, among them custom variables. And that is the trick: in the place where you use the icon, you set a variable to a certain value. And inside the icon template, you assign the value of this variable to a property.
For icons,
<use>
and<symbol>
have had its use…It can be done much more semantic, with native Web Components, supported in all browsers:
Create a
<template is="ICON-NAME">
for every SVG icon, including all its [state] CSS/animation stylesYou can design each SVG in a CodePen and copy it whole into the
<template>
Define a native Web Component
<svg-icon>
that clones the
<template>
in shadowDOM. So all CSS from the template is scoped!GZIP and Brotli love repetitions, so put all templates in the
<head>
you already said you are working on a PWA, so indeed no need to cache anything; if not, use my load-file Web Component to load external HTML Templates.
Minor detail; I removed the , (comma) from the d-paths, since that is a hardly used character in HTML. A more frequently used space character will compress better.
Make sure you use
<svg-icon>
in the DOM after the<template>
are defined (or add async load logic)Attaching the shadowDOM in the
connectedCallback
(usually done in theconstructor
) because we need DOM information: theis
attribute fromsvg-icon
(not 100% sure) I think your
:host
experiment did not work because you tried to apply it to a user-agent (==Browser!) shadowRoot created by<use>
(like<input>
and<textarea>
also create). They are more like locked down IFrames.Those are a different shadowRoots than the
open
(or hardly usedclosed
) shadowRoots us mortals in Userland (not user-agent!) create with Web Components.extends HTMLElement
(or not support Safari)You probably want to extend the
onclick
function to emit a CustomEvent bubbling up through shadowDOM (withcomposed:true
) emitting the iconname your PWA can respond to.https://yqnn.github.io/svg-path-editor/# is great for editing single d-paths, converting them to relative notation.