webdevpuneet.com

Vivus SVG Line Draw — Self-Drawing SVG Animation

Vivus SVG Line Draw · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

Requires
<div class="vsd-wrap">
  <div class="vsd-stage">
    <svg id="vsdSvg" viewBox="0 0 240 240" class="vsd-svg" aria-label="Line-art compass badge drawing itself">
      <circle cx="120" cy="120" r="106"/>
      <circle cx="120" cy="120" r="94"/>
      <line x1="120" y1="14" x2="120" y2="30"/>
      <line x1="226" y1="120" x2="210" y2="120"/>
      <line x1="120" y1="226" x2="120" y2="210"/>
      <line x1="14" y1="120" x2="30" y2="120"/>
      <circle cx="158" cy="76" r="15"/>
      <path d="M46 162 L92 98 L120 134 L150 88 L194 162"/>
      <path d="M92 98 L110 122 L98 134"/>
      <line x1="40" y1="162" x2="200" y2="162"/>
      <path d="M64 182 Q92 172 120 182 T176 182"/>
    </svg>
  </div>

  <div class="vsd-panel">
    <span class="vsd-tag">vivus · stroke-dashoffset</span>
    <h2>Draw it, don't fade it</h2>
    <p>Vivus measures every path and animates its dash offset, so the artwork appears to be drawn by hand.</p>

    <div class="vsd-row">
      <button class="vsd-play" id="vsdPlay">Replay</button>
      <div class="vsd-types" id="vsdTypes">
        <button class="vsd-chip is-on" data-type="delayed">delayed</button>
        <button class="vsd-chip" data-type="oneByOne">oneByOne</button>
        <button class="vsd-chip" data-type="sync">sync</button>
      </div>
    </div>

    <label class="vsd-scrub">
      <span>Scrub <b id="vsdVal">100%</b></span>
      <input type="range" id="vsdRange" min="0" max="100" value="100">
    </label>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Automatic path measuring
Vivus calls getTotalLength on every drawable element for you.
Three timing modes
delayed, oneByOne, and sync switchable live on the same SVG.
Manual start
start: manual means you decide the trigger — button, scroll, or hover.
Draggable timeline
setFrameProgress turns the draw into a scrubbable animation.
Live progress readout
currentFrame over frameLength polled in a rAF loop during playback.
Clean instance rebuild
destroy() restores dash attributes before a new instance measures.
Themeable strokes
Stroke color and width live in CSS, not SVG attributes.
Round caps and joins
Stroke ends stay soft while the path is mid-reveal.

About this UI Snippet

Vivus SVG Line Draw — How Stroke-Dashoffset Animation Actually Works

Screenshot of the Vivus SVG Line Draw snippet rendered live

A logo that draws itself is one of the few web animations that still stops people mid-scroll. It also looks like it must involve a video or a Lottie file, and it involves neither — it is one CSS property, applied cleverly.

The trick underneath

Every SVG stroke supports stroke-dasharray, which turns a solid line into a dashed one, and stroke-dashoffset, which slides those dashes along the path. Set the dash length to exactly the total length of the path and you get a single dash covering the whole line, with a single gap of the same size after it. Push the offset to that same length and the dash is pushed entirely out of view — the path is invisible. Animate the offset back to zero and the line appears to be drawn from one end to the other.

Doing it by hand means calling path.getTotalLength() on every element, writing two properties per path, and hand-scheduling the timing. Vivus is a 5kb library that does exactly that, for every drawable element in an SVG, automatically.

What the three types actually change

The type option is the whole personality of the animation, and the three modes here are meaningfully different:

- delayed — every path animates over the full duration, but each starts at a slightly staggered offset. Strokes overlap heavily, so the drawing appears to emerge everywhere at once while still resolving in order. This is the most "designed" looking option and the default here. - oneByOne — each path waits for the previous to finish completely. The duration is divided by total path length, so long paths take proportionally longer. This is the literal "someone is drawing this" reading, and it is much slower to complete. - sync — every path starts and ends together. The whole illustration materializes as one, which suits geometric marks and looks wrong on illustrative ones.

Because oneByOne and delayed derive their schedule from measured path lengths, document order matters — the SVG here is authored deliberately, with the outer rings first and the foreground detail last, so the drawing builds from frame to subject.

Manual start, and driving it yourself

start: 'manual' is what makes this snippet interactive rather than a fire-and-forget intro. Nothing plays until .play() is called, which means you can trigger on scroll with an IntersectionObserver, on hover, or on a button as done here.

The scrubber is the more interesting half. vivus.setFrameProgress(0.5) jumps the drawing to any point between 0 and 1 instantly — so the animation becomes a timeline you can drag, not just something you watch. Wiring that to a scroll position instead of a range input is the scroll-driven logo-draw effect seen on agency sites, and it is one line different from what is here.

Keeping the slider in sync *during* playback needs a small loop, because Vivus does not emit progress events. It exposes currentFrame and frameLength, so a requestAnimationFrame poll converts them to a percentage while playing is true and stops the moment the completion callback fires.

The gotcha: destroy before rebuilding

Switching the timing type requires a new Vivus instance, and this line is not optional:

if (vivus) vivus.destroy();

Vivus mutates the SVG it is given, writing stroke-dasharray and stroke-dashoffset inline on every path. Constructing a second instance over the same markup makes it measure paths that already carry the previous run's dash values, and the result is an animation that starts half-drawn or never completes. destroy() restores the original attributes so the new instance measures clean geometry. This is the single most common Vivus bug and it only appears once you make the animation reconfigurable.

Authoring SVG that Vivus can draw

The requirement is simple and absolute: Vivus animates strokes, not fills. Every element here is fill: none with an explicit stroke and stroke-width, set in CSS rather than as attributes so the artwork stays themeable. Shapes that are filled rather than stroked are skipped entirely — which is why exporting a logo from a design tool usually produces nothing until the fills are converted to outlined strokes.

stroke-linecap: round and stroke-linejoin: round matter more than they look: a drawing animation constantly reveals path *ends*, and square caps make every in-progress stroke read as cut off.

Reusing it

Drop in your own line-art SVG, keep everything fill: none with strokes, and order the paths the way you want them drawn. Trigger play() from an IntersectionObserver for a scroll-triggered logo intro, or feed scroll progress into setFrameProgress() for a scrubbed one. It pairs naturally with a draw SVG success checkmark for micro-feedback, or morph SVG icons when shapes need to become other shapes rather than appear.

Step by step

How to Use

  1. 1
    Add the Vivus CDNInclude vivus from the CDN panel — one script, global Vivus.
  2. 2
    Paste HTML, CSS, and JSThe compass badge draws itself as soon as the snippet loads.
  3. 3
    Press ReplayThe instance resets to zero length and redraws from the start.
  4. 4
    Switch timing typesCompare delayed, oneByOne, and sync on the same artwork.
  5. 5
    Drag the scrubbersetFrameProgress jumps the drawing to any point in its timeline.
  6. 6
    Swap in your own SVGUse fill:none stroked paths and order them the way you want them drawn.

Real-world uses

Common Use Cases

Logo intros
Draw a brand mark on first load rather than fading it in, with Vivus measuring every path using getTotalLength automatically.
Scroll-triggered illustrations
Feed scroll progress into setFrameProgress, so a drawing builds as the visitor moves down the page.
Onboarding and empty states
Show line art that builds itself while a user waits, with start: manual letting you trigger it from a button or hover.
Success confirmations
Offer a larger sibling of the draw SVG success tick, with delayed, one-by-one and sync timing modes switchable live.
Infographic reveals
Build diagrams stroke by stroke as a section enters view, and use the empty state pattern to hold the space before it starts.

Got questions?

Frequently Asked Questions

stroke-dasharray sets the dash length to the path total length, so there is one dash covering the whole line. stroke-dashoffset then pushes that dash entirely out of view, making the path invisible. Animating the offset back to zero slides the dash into place, which reads as the line being drawn. Vivus measures every path with getTotalLength and applies both properties automatically.

delayed animates every path over the full duration but staggers their start times, so strokes overlap and the whole drawing emerges together while still resolving in order. oneByOne waits for each path to finish before starting the next, which is the most literal hand-drawn reading and takes longest. sync starts and ends every path simultaneously, which suits geometric marks and looks wrong on illustrations.

Vivus mutates the SVG, writing stroke-dasharray and stroke-dashoffset inline on every path. A second instance built over the same markup measures paths that still carry the previous run values, so the animation starts half-drawn or never completes. destroy() restores the original attributes so the new instance measures clean geometry.

Almost always because the shapes are filled rather than stroked. Vivus animates strokes only — every element needs fill: none with an explicit stroke and stroke-width. Logos exported from design tools are usually filled outlines, so they must be converted to stroked paths first.

Vivus does not emit progress events, but it exposes currentFrame and frameLength. A requestAnimationFrame loop divides one by the other while a playing flag is true and writes the percentage to the range input, stopping when the completion callback flips the flag.

Create the Vivus instance in a mount effect against a ref to the SVG element, never during render, since it measures live geometry. Call instance.destroy() in the cleanup so remounts do not stack mutations on the same markup. Keep the instance in a ref rather than state, and rebuild it in an effect keyed to the timing type when that changes.