SVG that draws itself
Hand-drawn line work is the fastest way to make a page feel authored rather than assembled, and until 2025 the plugin that does it was behind a paywall — DrawSVGPlugin is now free, and most people have not noticed. It animates stroke-dasharray and stroke-dashoffset for you, in percentages, on any stroked shape. What makes the difference between a party trick and something worth shipping is which of the three timing models you pick: a flourish should fire once and be over, a chart should be scrubbed so the reader draws it themselves, and a progress ring should animate to a value that means something. Same property, three completely different jobs.
How to build
Script only
Free plan gets you part of the way
The panel covers some of this. The rest needs the script, which needs a paid plan — build the panel half now, come back for the rest.
How the two routes work →Plugins
DrawSVGPlugin · ScrollTrigger
Build time
45 min. Getting the SVG right takes longer than animating it.
No-code?
No. DrawSVG is not exposed in the Interactions panel. In Webflow the SVG goes into an HTML Embed and the animation into a script — but the shape itself can be pasted straight out of Figma.
Webflow structure
element · class · attribute<i>1 · the flourish, inside the heading</i> <b>Heading H1</b> <i>heading-style-h1</i> <b>Span</b> <i>heading_underline</i> ← position: relative; display: inline-block <b>HTML Embed</b> ← the <svg>, absolutely positioned under the word <i>2 · the chart</i> <b>Div</b> <i>chart_card</i> <b>Div</b> <i>chart_head</i> <b>HTML Embed</b> <i>chart</i> <svg viewBox="0 0 860 280"> <g class="chart_grid">…</g> <path class="chart_area" /> ← fill, opacity 0 <path class="chart_line" /> ← stroke, no fill <circle class="chart_dot" /> × n </svg> <i>3 · the rings</i> <b>Div</b> <i>ring</i> <b>HTML Embed</b> <svg viewBox="0 0 82 82"> <circle class="bg" /> <circle class="fg" data-pct="94" /> </svg>
Styles to set in the Designer
chart_line | <code>fill: none</code> is mandatory. A path with a fill and no stroke has nothing to draw — this is the number one reason DrawSVG appears to do nothing. chart_line | <code>stroke-linecap: round; stroke-linejoin: round;</code>. The difference between a drawn line and a technical diagram. heading_underline path | <code>vector-effect: non-scaling-stroke</code> — the SVG is stretched to the width of the word, and without this the stroke stretches with it and goes lumpy. heading_underline svg | <code>preserveAspectRatio="none"</code> plus <code>overflow: visible</code>, absolutely positioned under the word. It must distort horizontally to match the text, which is exactly what the non-scaling stroke protects against. ring svg | <code>transform: rotate(-90deg)</code> on the SVG so 0% starts at twelve o'clock instead of three. ring circle | <code>fill: none; stroke-linecap: round;</code>. The <code>fg</code> circle sits exactly on top of the <code>bg</code> one — same cx, cy and r.
Building it with the Interactions panel
no code, free planWhat the panel still can’t do. Only custom code can close these gaps — there is no panel setting for any of them.
See “The script” below for the exact code.
Custom attributes
Element settings → Custom attributesring .fg circle | data-pct | 94 | How far round to draw. The script reads it and animates <code>drawSVG</code> to <code>"0% 94%"</code>.
CSS
Page settings → Inside <head> tagThe script
Page settings → Before </body> tag<!-- Page settings → Before </body> tag --> <script> document.addEventListener("DOMContentLoaded", () => { gsap.registerPlugin(DrawSVGPlugin, ScrollTrigger); if (matchMedia("(prefers-reduced-motion: reduce)").matches) { gsap.set(".chart_area", { opacity: 1 }); return; } // ── 1 · flourish: fires once, and is over ─────────────────────────────────── gsap.fromTo(".heading_underline path", { drawSVG: "0%" }, { drawSVG: "100%", duration: 0.9, ease: "power2.inOut", scrollTrigger: { trigger: ".heading-style-h1", start: "top 72%", once: true } }); // ── 2 · chart: scrubbed, so the reader draws it ───────────────────── gsap.timeline({ scrollTrigger: { trigger: ".chart", start: "top 82%", end: "top 35%", scrub: 0.6, // a little lag reads as ink flowing invalidateOnRefresh: true } }) .fromTo(".chart_line", { drawSVG: "0%" }, { drawSVG: "100%", ease: "none" }, 0) .fromTo(".chart_area", { opacity: 0 }, { opacity: 1, ease: "none" }, 0.15) // Dots staggered along the SAME timeline, so one can never appear before // the line has reached it. .fromTo(".chart_dot", { scale: 0, transformOrigin: "50% 50%" }, { scale: 1, ease: "back.out(2)", stagger: { each: 0.08, from: "start" } }, 0.05); // ── 3 · rings: animate to a number that means something ───────────── document.querySelectorAll(".ring .fg").forEach(ring => { const pct = parseFloat(ring.dataset.pct) || 0; gsap.fromTo(ring, { drawSVG: "0% 0%" }, { drawSVG: "0% " + pct + "%", duration: 1.4, ease: "power3.out", scrollTrigger: { trigger: ring, start: "top 88%", once: true } }); }); }); </script>
Gotchas that will cost you an hour
- fill: none and a real stroke. DrawSVG works on the stroke. Export a shape from Figma as a filled path and the plugin runs perfectly and nothing visibly happens.
- Strip Figma's own stroke-dasharray. Exported SVGs often carry one, and it fights the plugin — you get a dashed line that draws in fragments.
- vector-effect: non-scaling-stroke on anything stretched. An underline sized to a word is squashed horizontally; without this the stroke squashes too and the line looks thin at the ends and fat in the middle.
- Rotate the ring's SVG, not its circle. Rotating the circle moves its centre and the arc drifts off the track. Rotate the whole <svg> by −90°.
- Stagger the dots on the line's own timeline. A separate trigger means that on a fast scroll the dots can appear ahead of the line, which instantly reads as broken.
- Give the SVG a viewBox and no fixed width/height. Fixed dimensions make it non-responsive, and in Webflow an embedded SVG with hard-coded sizes will overflow its column on mobile.
- Add a real role="img" and aria-label to a chart. A drawing animation is decorative; the data it shows is not.
Push it further
- Draw a route across a map with the same scrub, and put a marker on it with MotionPathPlugin so something travels the line as it draws.
- Morph between two chart shapes with MorphSVGPlugin — also free now — for a toggle between monthly and quarterly views.
- Pair the ring with a counter so the number and the arc reach their value on exactly the same timeline.
Webflow Motion Lab — GSAP 3.15, all plugins free.
18
/ 30