The general gist is that we replace normal link underlines (text-decoration: underline
) with an SVG background image. The background image is positioned along the bottom of the element, prevented from repeating vertically, and then scaled by defining the background size in text units:
a {
text-decoration: none;
}
a:hover {
background-position: bottom;
background-repeat: repeat no-repeat;
background-size: 1em .5em; /* Scale to text size -- has to be same aspect ratio as the SVG */
background-image: url('./underline.svg');
}
To make things easier to tweak (and use one less HTTP request) we can inline the SVG into the CSS as a data URL. So it's still easy to read and edit I've split it into multiple lines, but it's important to note that all the lines must end with a backslash (\
) when doing this:
a {
text-decoration: none;
}
a:hover {
background-position: bottom;
background-repeat: repeat no-repeat;
background-image: url('data:image/svg+xml;charset=utf8,\
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8" viewBox="0 0 16 8">\
<rect x="0" y="0" width="16" height="8" fill="white"/>\
</svg>\
');
}
Also remember - this is now being treated as a URL, so we have to be careful about using characters that aren't URL-safe (namely
<
, >
, #
and &
) and manually percent-encode them. For example if you wanted to use a hex color, you'd need to replace the #
with %23
.
With a bit of SCSS we can make things nicer by automatically url-encoding the SVG string:
// Bare-minimum url-safe string encoder
@function urlencode($str) {
$tokens: (
'<': '%3C',
'>': '%3E',
'#': '%23',
'"': '\'',
'&': '%26'
);
$result: '';
// loop through the string, replacing characters as necessary
@for $i from 1 through str-length($str) {
$char: str-slice($str, $i, $i);
@if map-has-key($tokens, $char) {
$char: map-get($tokens, $char);
}
$result: $result + $char;
}
@return $result;
}
// Inline svg url shorthand
@function inline-svg($str) {
@return url(urlencode('data:image/svg+xml;charset=utf8,' + $str));
}
Now we don't have to worry about manually url-encoding or writing out the mime type each time;
a:hover {
background-position: bottom;
background-repeat: repeat no-repeat;
background-size: 1em .5em; // Scale to text size -- has to be same aspect ratio as the SVG
background-image: inline-svg('\
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8" viewBox="0 0 16 8">\
<rect x="0" y="0" width="16" height="8" fill="#fff"/>\
</svg>\
');
}
Another nice benefit is that we can also use SCSS variables within the inline SVG, by e.g. replacing fill="red"
with fill="#{ $accent-color }"
.
Now for the trick! We can actually embed CSS into the inline SVG to create keyframed animations!
a:hover {
background-position: bottom;
background-repeat: repeat no-repeat;
background-size: 1em .5em; // Scale to text size -- has to be same aspect ratio as the SVG
background-image: inline-svg('\
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8" viewBox="0 0 16 8">\
<style>\
@keyframes p {\
from {\
transform: translateX(0);\
}\
to {\
transform: translateX(16px);\
}\
}\
</style>\
<path\
style="animation: p 2s infinite linear"\
fill="none"\
stroke-width="1"\
stroke="#fff"\
d="M -16,4 q 4,4, 8,0 q 4,-4, 8,0 q 4,4, 8,0 q 4,-4, 8,0 q 4,4, 8,0 q 4,-4, 8,0"\
/>\
</svg>\
');