In this post, I'll share my journey of building a reusable Svelte component to draw SVG on hover. Begin by trying to reduce boilerplate in my work project and publishing my first npm package.
It's great if you've already known below things:
- Svelte Transition
-
Svelte Component Composition
This is my npm package: Svelte Hover Draw SVG
Let's talk about motivation
In my work project, I have a Tab component with inline SVG in each tab:
I like to add SVG animation on hover, and the first that came to my mind was transition:draw
in Svelte:
It works, but here are the problems:
- Boilerplates like
on:mouseenter
,{#if cursorOn === 'tab'}
, andtransition:draw
- SVG might have multiple
<path>
,line
,rect
,polyline
,polygon
. We need to addtransition
to each of them manually - We can't use it when the SVG is in a component like
<SVGInComponent/>
(draw
can only be used onSVGElement
or element which hasgetTotalLength()
method.)
Can we DRY them up?
DRY = don't repeat yourself
I don't want to write boilerplate again and again if I just want this simple effect.
So I started to find a way to abstract the hovering event & effect.
First thing we need to know is the draw
animation is actually a CSS transition:
// Please refer to https://svelte.dev/tutorial/custom-css-transitions
function draw(node, { delay = 0, speed, duration, easing: easing$1 = easing.cubicInOut } = {}) {
let len = node.getTotalLength();
const style = getComputedStyle(node);
if (style.strokeLinecap !== 'butt') {
len += parseInt(style.strokeWidth);
}
if (duration === undefined) {
if (speed === undefined) {
duration = 800;
}
else {
duration = len / speed;
}
}
else if (typeof duration === 'function') {
duration = duration(len);
}
return {
delay,
duration,
easing: easing$1,
css: (t, u) => `stroke-dasharray: ${t * len} ${u * len}`
};
}
This is the source code of draw
, the key part is:
css: (t, u) => `stroke-dasharray: ${t * len} ${u * len}
When the transition
is triggered, a CSS animation with custom @keyframes
would be added to that element:
So what we're trying to do is quite simple: toggling a CSS animation on hover!
A Trick That Makes Drawing SVG Lines Way Easier
I was looking for a way to simplify the complex @keyframes
, and I found this article: A Trick That Makes Drawing SVG Lines Way Easier by Chris Coyier.
By setting the pathLength="1"
to SVGElement
(like <path>
, line
, rect
, polyline
, and polygon
), set the stroke-dasharray
to 1, and animate the offset in CSS!
<path class="path" d="M66.039,133.545 ... " pathLength="1" />
.path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: dash 5s linear alternate infinite;
}
@keyframes dash {
from {
stroke-dashoffset: 1;
}
to {
stroke-dashoffset: 0;
}
}
I also found that I can put this path
class to elements that have SVG inside, which means we don't need to add the animation to SVGElement
programmatically:
<div class="tabs">
<a href="#tab1" class="tab">
<svg stroke-width="2">
<!-- This works -->
<path class="path" pathLength="1" d="..." />
</svg>
<span>Tab 1</span>
</a>
<!-- This also works -->
<a href="#tab2" class="tab path">
<svg stroke-width="2">
<path pathLength="1" d="..."/>
</svg>
<span>Tab 2</span>
</a>
</div>
Make a wrapper component for abstraction
The goal of creating a wrapper component:
- Let the wrapper component handle the hover event & effect.
- Trigger animation on hover (component level, not just SVG).
- User can just put their components/ SVGs in the wrapper component.
Here is the final approach:
You can try it out here: Svelte REPL
Easy to use + less code to write FTW 💪
- Inline SVG:
- SVG Component (set draw time to 2s, default is 1s):
- Nested elements (expose
hovering
status):
Wrapping up
There were some technical considerations I didn't mention in this article and finally gave up on, like designing a custom transition and using named slots to validate SVGElement
, ...etc.
It'd be great to let users customize the transition using the native parameters like duration
and easing
, but it's overkill if this simple effect can be achieved by CSS.
That's all. Thank you for your reading! 🙌
You can find me on Twitter: @davipon
Top comments (0)