DEV Community 👩‍💻👨‍💻

Cover image for Build a Svelte Component To Draw SVG On Hover
David Peng
David Peng

Posted on

Build a Svelte Component To Draw SVG On Hover

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:

This is my npm package: Svelte Hover Draw SVG

Any feedbacks are welcomed!
github cover

Let's talk about motivation

In my work project, I have a Tab component with inline SVG in each tab:
tabs

I like to add SVG animation on hover, and the first that came to my mind was transition:draw in Svelte:

example-1

It works, but here are the problems:

  1. Boilerplates like on:mouseenter, {#if cursorOn === 'tab'}, and transition:draw
  2. SVG might have multiple <path>, line, rect, polyline, polygon. We need to add transition to each of them manually
  3. We can't use it when the SVG is in a component like <SVGInComponent/> (draw can only be used on SVGElement or element which has getTotalLength() method.)

Can we DRY them up?

DRY

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}`
  };
}
Enter fullscreen mode Exit fullscreen mode

This is the source code of draw, the key part is:

 css: (t, u) => `stroke-dasharray: ${t * len} ${u * len}
Enter fullscreen mode Exit fullscreen mode

When the transition is triggered, a CSS animation with custom @keyframes would be added to that element:

dev console

dev console2

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" />
Enter fullscreen mode Exit fullscreen mode
.path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: dash 5s linear alternate infinite;
}

@keyframes dash {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

source-code

You can try it out here: Svelte REPL

Easy to use + less code to write FTW 💪

  • Inline SVG:

inline-svg

  • SVG Component (set draw time to 2s, default is 1s):

svg-component

  • Nested elements (expose hovering status):

nested-svg

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)

🌚 Life is too short to browse without dark mode