
✨ 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: LENGTHand animatestroke-dashoffsetfromLENGTHto0. - To reverse direction, swap stroke and gap in
stroke-dasharrayand pushstroke-dashoffsetforward. - 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 atFULL_LENGTH. - Set up double lines via half-length
stroke-dasharrayand quarter-shiftstroke-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 } 6 svg 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_LENGTHwith the actual number without units of measurement. The CSS engine doesn't understandpxoreminsidestroke-dasharray. - The
svg pathselector is for one contour. If the SVG has multiple paths, use classes, IDs, or pseudo-classes likepath:nth-of-type(3). Debugging tools::nth Testerand nthmaster.com. - The
@keyframesname must be unique for each animation variant. Different paths often require their own keyframes. - Avoid negative
stroke-dashoffsetvalues: 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 } 11 svg 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 } 9 svg 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 } 11 svg 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 } 11 svg 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 } 11 svg 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 } 9 svg 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 } 11 svg 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 } 11 svg 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 } 11 svg .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
@keyframesor 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
forwardstoanimation(preserves final state) and removealternateif you don't need reversal. Also check for conflicts with other CSS rules for the same element, for exampletransitionon 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.



