Skip to content

Everything for WordPress, web development — and beyond

✨ Animating SVG lines and closed paths with pure CSS

✨ Animating SVG lines and closed paths with pure CSS

SVG stroke animation is one of those techniques that transforms a static icon into a living illustration. You've probably seen this effect: a logo outline smoothly draws itself, as if an invisible pen traces the shape from start to finish. It looks expensive, but it's done with pure CSS.

The problem is that most tutorials stop at the simplest case: an open path, a single line, animation from start to end. But real graphics involve closed contours, broken shapes, and drawing directions that the designer didn't control. An SVG path can start anywhere, and you need it to draw clockwise from an aesthetically pleasing starting point.

Below is the complete set of techniques: from basic animation to double lines and full control over the start and end points. All in pure CSS, no libraries or runtime measurements. These methods were tested while developing the SVG Divider plugin and work in any modern browser.

💡 Quick overview:

  • Find the SVG path length via getTotalLength() or Illustrator and write the number into CSS.
  • Set stroke-dasharray: LENGTH and animate stroke-dashoffset from LENGTH to 0.
  • To reverse direction, swap stroke and gap in stroke-dasharray and push stroke-dashoffset forward.
  • Shift the starting point by adding half (or any fraction) of the length to stroke-dashoffset.
  • Launch "inside-out" animation: start from HALF_LENGTH, finish at FULL_LENGTH.
  • Set up double lines via half-length stroke-dasharray and quarter-shift stroke-dashoffset.

Technical foundation: strokes, gaps, and path length

At the core of any SVG animation are two CSS properties: stroke-dasharray and stroke-dashoffset. The first defines a "stroke-gap" pattern along the contour, the second shifts this pattern. If you make the stroke equal to the full path length and then animate the offset, the line "draws" before your eyes.

The key step before any animation is finding the full path length. Methods:

  • JavaScript: document.querySelector('path').getTotalLength(), one console call, the number goes hard-coded into CSS. No runtime.
  • Illustrator: the "Document Info" panel shows the length of the selected path.
  • Custom script: if you have dozens or hundreds of paths, it makes sense to write automatic traversal and length markup.

Important nuance: getTotalLength() and Illustrator may give slightly different numbers for the same path. The reason is different calculation precision. The discrepancy is usually within 1-2 units, doesn't affect visuals.

Check the path geometry for errors. The drawing direction and starting point depend on how the designer built the curve in the editor, that's normal. Below we'll learn to control both without editing the SVG itself.

Basic animation: drawing a line from start to end

Simplest case: an open path with a length of, say, 100 units. Set the stroke to full length and animate the offset from 100 to 0:

1@keyframes reveal {
2 to {
3 stroke-dashoffset: 0;
4 }
5}
6svg path {
7 stroke-dashoffset: 100;
8 stroke-dasharray: 100;
9 animation: reveal 1s ease-in-out 0s forwards;
10}

What's happening here: stroke-dasharray: 100 sets 100 units of solid line and 100 units of empty space (the second value defaults to the first). stroke-dashoffset: 100 shifts the pattern so the visible part goes beyond the path boundaries, the line is invisible. Animation reduces the offset to zero, and the stroke crawls into place.

A few practical notes:

  • Replace FULL_LENGTH with the actual number without units of measurement. The CSS engine doesn't understand px or em inside stroke-dasharray.
  • The svg path selector is for one contour. If the SVG has multiple paths, use classes, IDs, or pseudo-classes like path:nth-of-type(3). Debugging tools: :nth Tester and nthmaster.com.
  • The @keyframes name must be unique for each animation variant. Different paths often require their own keyframes.
  • Avoid negative stroke-dashoffset values: Safari may flicker or show artifacts with negative offsets.

Controlling direction: reversing the drawing

By default, animation goes from the path start to end, wherever the designer drew the curve. But you often need the opposite: a closed contour drawing clockwise instead of counterclockwise.

The solution is to invert the stroke pattern: the line starts invisible (0 stroke, 100 gap) and becomes visible (100 stroke, 0 gap), while the offset pulls it backward:

1@keyframes reversed {
2 from {
3 stroke-dasharray: 0 100;
4 stroke-dashoffset: 0;
5 }
6 to {
7 stroke-dasharray: 100 0;
8 stroke-dashoffset: 100;
9 }
10}
11svg path {
12 animation: reversed 1s ease-in-out 0s forwards;
13}

The mechanics work through the cyclical nature of the stroke pattern: the solid line materializes at the path end, while its copy goes left beyond the start and remains invisible. The same happens on closed contours: the stroke copy appears "before" the start and doesn't interfere with perception.

There's an alternative approach, shorter and without array manipulation:

1@keyframes alternative-reversed {
2 from {
3 stroke-dashoffset: 100;
4 }
5 to {
6 stroke-dashoffset: 200;
7 }
8}
9svg path {
10 stroke-dasharray: 100;
11 animation: alternative-reversed 1s ease-in-out 0s forwards;
12}

Here stroke-dasharray is fixed at full length, while the offset goes from 100 to 200. Same result, the line draws in reverse direction. Use whichever variant feels more intuitive to you.

Shifting the starting point

A closed path is like a circular railroad: the train can start from any station. The task is to shift the animation start to an aesthetically pleasing point without touching the geometry itself.

Starting from the opposite side

For symmetrical contours, half-length offset is enough:

1@keyframes start-from-other-side {
2 from {
3 stroke-dasharray: 0 100;
4 stroke-dashoffset: 50;
5 }
6 to {
7 stroke-dasharray: 100 0;
8 stroke-dashoffset: 50;
9 }
10}
11svg path {
12 animation: start-from-other-side 1s ease-in-out 0s forwards;
13}

The drawing direction remains original, but visually the animation starts from the opposite end.

Reversing direction and arbitrary start

To simultaneously reverse direction and shift the starting point, add full length to the final offset:

1@keyframes other-side-and-reverse {
2 from {
3 stroke-dasharray: 0 100;
4 stroke-dashoffset: 50;
5 }
6 to {
7 stroke-dasharray: 100 0;
8 stroke-dashoffset: 150;
9 }
10}
11svg path {
12 animation: other-side-and-reverse 1s ease-in-out 0s forwards;
13}

For a completely arbitrary point, substitute any value instead of 50. Trial and error (plus or minus 5-10 units) finds the optimal position. In one project, the starting point on a Twitch bit icon was shifted by 17 pixels, an ugly number but visually perfect.

"Inside-out" animation

The line grows from the middle in both directions simultaneously, a classic technique for logos and infographics:

1@keyframes inside-out {
2 from {
3 stroke-dasharray: 0 100;
4 stroke-dashoffset: 50;
5 }
6 to {
7 stroke-dasharray: 100 0;
8 stroke-dashoffset: 100;
9 }
10}
11svg path {
12 animation: inside-out 1s ease-in-out 0s forwards;
13}

The offset starts from half the length (path center) and goes to full length, pulling the stroke along. For asymmetric shapes, the starting point can be additionally shifted:

1@keyframes custom-inside-out {
2 from {
3 stroke-dasharray: 0 100;
4 stroke-dashoffset: 84;
5 }
6 to {
7 stroke-dasharray: 100 0;
8 stroke-dashoffset: 134;
9 }
10}

Here 84 = HALF_LENGTH + 34. The constant 34 is hand-picked for a specific shape. There's no universal recipe, try, look, adjust.

"Outside-in" animation: double pen

The opposite approach: the line draws from both ends simultaneously, converging to the middle. On open contours this gives a "two pens" effect:

1@keyframes outside-in {
2 from {
3 stroke-dasharray: 0 100;
4 }
5 to {
6 stroke-dasharray: 50 0;
7 }
8}
9svg path {
10 animation: outside-in 1s ease-in-out 0s forwards;
11}

The stroke reaches only half the length, the gap collapses twice as fast as in basic animation. stroke-dashoffset isn't needed here: the "0 gap" pattern in the final state by itself makes both stroke halves converge.

Full control: starting and ending points

When one direction and starting point aren't enough, you can control both drawing boundaries. Two moving line heads converge to a common destination:

1@keyframes custom-start-end {
2 from {
3 stroke-dasharray: 0 100;
4 stroke-dashoffset: 30;
5 }
6 to {
7 stroke-dasharray: 100 0;
8 stroke-dashoffset: 80;
9 }
10}
11svg path {
12 animation: custom-start-end 1s ease-in-out 0s forwards;
13}

The values 30 and 80 are arbitrary; from-offset less than to-offset is the only hard rule. If the points are close, one segment flies fast, the other crawls slowly, that's normal and even beautiful.

Don't be afraid to go beyond full length in the to value. stroke-dashoffset: 130 with length 100 is legal, it just shifts the stroke cycle to the second round. The main thing is to avoid negative values.

Double lines and special effects

The most flexible mode: half-length strokes with quarter-shift. Each path gets two line heads moving independently:

1@keyframes double-lines {
2 from {
3 stroke-dasharray: 0 50;
4 stroke-dashoffset: 25;
5 }
6 to {
7 stroke-dasharray: 50 0;
8 stroke-dashoffset: 50;
9 }
10}
11svg path {
12 animation: double-lines 1s ease-in-out 0s forwards;
13}

For complex shapes, the stroke array can consist of multiple components. The sum of all array values should equal full length. Real example for an infinity sign:

1@keyframes infinity-double-lines {
2 from {
3 stroke-dasharray: 0 53 0 47;
4 stroke-dashoffset: 37;
5 }
6 to {
7 stroke-dasharray: 53 0 47 0;
8 stroke-dashoffset: 60;
9 }
10}
11svg .infinity {
12 animation: infinity-double-lines 8s ease-in-out 0s alternate infinite;
13 stroke: #8D69D8;
14}

Four array components (53 + 47 = 100 = full length) swap places between from and to. The animation loops (infinite) and reverses (alternate), creating a meditative breathing contour that never repeats identically.

Video: SVG animation in practice

To see all the described techniques live and understand setup nuances, watch this English tutorial:

The author shows step-by-step creation of an animated SVG illustration from export from Figma to final CSS touches. Timecodes in the description let you jump straight to the needed section.

⁉️🤔 Frequently asked questions

Why know the path length in advance if there's getTotalLength()?

getTotalLength() is JavaScript, and our goal is pure CSS without runtime dependencies. You measure the length ONCE (in console or Illustrator), hard-code the number into CSS, and the animation works without a single line of JS.

Can you animate multiple paths with one animation?

Yes, if they have the same length and desired behavior. In practice, paths rarely match, it's easier to give each its own @keyframes or use CSS variables with different length values.

Why does Safari flicker with negative stroke-dashoffset?

It's a known WebKit rendering bug: negative offset values are sometimes interpreted as instant reset to 0. The fix is to always keep stroke-dashoffset ≥ 0, if you need a shift, add full length instead of going negative.

What to do if the animation "jerks" at start or end?

Add forwards to animation (preserves final state) and remove alternate if you don't need reversal. Also check for conflicts with other CSS rules for the same element, for example transition on the same property.

Does this work with stroke-dasharray on closed contours with complex geometry?

Yes, the mechanics are the same for any SVG paths, straight lines, Bezier curves, closed polygons. The only difference is that on a closed contour there's no visual "start" and "end," so controlling the starting point is especially important for aesthetics.

Is it worth writing animation manually when there are libraries?

Short answer: depends on scale. For a one-off icon on a landing page, manual control gives perfect results without extra dependencies. Fifteen minutes picking an offset, and the contour draws exactly as you intended, not as the library developer decided.

When we made SVG dividers for Elementor, manual control paid off three times over. The plugin uses dozens of animated dividers, each with individually tuned strokes. No library would give such control over aesthetics.

On top of the techniques from this article, you can layer:

  • Animation state control via JavaScript (start/stop/restart);
  • Lazy launch via Intersection Observer, the contour draws only when it enters the viewport;
  • Animation chains via animation-delay;
  • Custom easing functions (cubic-bezier());
  • Additional opacity work in @keyframes;
  • Elastic animation of dashed lines ("marching ants").

Take the blueprints from this article as a base, substitute your length values, and experiment with cubic-bezier(). A properly chosen easing function changes animation perception more than shifting the starting point by a couple pixels.