Issac Lau

How to animate SVG

January 22, 2025
8 min read
Loading

A site with a great experience often needs SVG icons as an essential element, because SVGs have natural advantages over other image formats.

They are simple, flexible, and customizable, and it is easy to add animation. Using this well can add a lively touch to your site.

This article introduces how to animate SVGs and shows some of the SVG animations used on this site.

SVG 101

SVG is an XML-based graphics format used to create shapes such as circles, rectangles, ellipses, lines, and text.

An SVG file contains an <svg> tag, which defines the graphics, including shape, color, and size.

Here is a simple SVG example:

function App() {
  return (
    <svg
      style={{ background: "green" }}
      width="200"
      height="200"
      viewBox="0 0 200 200"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <circle cx="100" cy="100" r="80" fill="#FF0000" />
    </svg>
  );
}

This SVG defines a circle. The cx and cy attributes specify the center, and r specifies the radius. Here are a few key SVG attributes worth calling out.

  • The width and height attributes specify the SVG size. They determine the overall space the SVG occupies in the page.
  • The viewBox attribute defines the SVG viewport. It is a string in the form x y width height, where x and y are the top-left coordinates of the viewport and width and height are the viewport size.

In other words, width and height control the SVG size in the HTML flow, while viewBox defines the internal coordinate system. The width and height inside viewBox are completely independent of the SVG's width and height.

The x and y in viewBox specify the top-left corner of the viewport. If omitted, the top-left is (0, 0). For convenience, we often adjust x and y so the SVG center is at (0, 0), which makes it easy to center elements.

function App() {
  return (
    <svg
      style={{ background: "green" }}
      width="200"
      height="200"
      viewBox="-100 -100 200 200"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <circle cx="0" cy="0" r="20" fill="blue" />
    </svg>
  );
}

By setting the viewBox x and y to -100, -100, the SVG center becomes (0, 0).

The width and height in viewBox specify the viewport size. Together with x and y, they define the coordinate system for child elements.

function App() {
  return (
    <svg
      style={{ background: "green" }}
      width="200"
      height="200"
      viewBox="-100 -100 100 100"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <circle cx="0" cy="0" r="20" fill="blue" />
    </svg>
  );
}

Compared with the previous example, we changed viewBox width and height. The coordinate system starts at (-100, -100), and with a viewBox size of 100 by 100, the bottom-right corner is (0, 0). That means the circle center is at the bottom-right, so only one quarter of the circle is visible.

This article does not go deep into SVG basics. If you want a solid introduction, I recommend MDN, or if you prefer learning with examples, SVG Tutorial is a great resource.

SVG animation techniques used on this site

Circular progress ring

On the blog list page, when you hover a blog card, a circular progress ring animates.

import { ArrowRightIcon } from "lucide-react";

function HoverCircle() {
  return (
    <span className="group cursor-pointer relative inline-flex items-center justify-center shrink-0 w-8 h-8">
      <svg
        className="absolute inset-0 w-full h-full text-primary/20"
        viewBox="0 0 100 100"
      >
        <circle
          cx="50"
          cy="50"
          r="48"
          fill="none"
          stroke="currentColor"
          strokeWidth="4"
        />
      </svg>
      <svg
        className="absolute inset-0 w-full h-full invisible group-hover:visible"
        viewBox="0 0 100 100"
      >
        <circle
          cx="50"
          cy="50"
          r="48"
          fill="none"
          stroke="currentColor"
          strokeWidth="4"
          className="origin-center -rotate-90 text-primary transition-all duration-500 ease-in-out group-hover:[stroke-dashoffset:0]"
          strokeDasharray="301"
          strokeDashoffset="301"
        />
      </svg>
      <ArrowRightIcon className="h-4 w-4 text-primary/50 relative z-10 transition-all duration-200 ease-in-out group-hover:text-primary group-hover:translate-x-1" />
    </span>
  );
}

This progress ring is built from two concentric circles. The lighter circle forms the track; the animation happens on the darker circle. The key is to use stroke-dasharray and stroke-dashoffset.

  • stroke-dasharray defines the pattern of dashes and gaps. Here, strokeDasharray="301" is equivalent to strokeDasharray="301,301" (odd values repeat). That means we draw a 301-length dash, then a 301-length gap. The circle radius is 48, so 301 is the circumference (48 * 3.14 * 2), and a 301-length dash draws the full circle.
  • stroke-dashoffset defines the offset between the dash pattern and the start of the path. If omitted, it defaults to 0.

Here we set strokeDashoffset="301", which shifts the entire dash off the path. In the default state, the upper circle is invisible and we see the gap. On hover, we animate strokeDashoffset to 0, which reveals the full circle and produces the progress ring effect.

Component styles use tailwindcss. If you're not familiar with it, I strongly recommend learning it.

You might have also noticed the like button icon on each blog post. If you're curious how it's built, stay tuned—I'll share more SVG animation techniques next time.

评论

Loading