Path Draw

A line illustration draws itself stroke by stroke, then a dot travels along the finished route.

  • CSS
  • SVG & shapes
  • Entrance
  • Added

AI prompt

Make an SVG line drawing animate itself in React with CSS. For each <path>, read its length with getTotalLength() and set it as --len; start with stroke-dasharray: var(--len) and stroke-dashoffset: var(--len), then animate stroke-dashoffset to 0 over 1.6s with cubic-bezier(0.65, 0, 0.35, 1), staggering each path by 250ms. Use stroke-linecap: round and fill: none. After the drawing finishes, send a small accent-coloured dot along the main route using offset-path with the same path data, animating offset-distance from 0% to 100% over 1.8s. Add a "Replay" button that restarts by changing a key. Give the SVG role="img" and an aria-label "Route from home to the studio". Under prefers-reduced-motion, show the finished drawing and keep the dot at the end.

Source

Demo.tsx
'use client'

import { useLayoutEffect, useRef, useState } from 'react'
import s from './demo.module.css'

const ROUTE = 'M20 120 C 60 120, 60 40, 110 40 S 160 110, 200 80 S 240 30, 260 30'

export default function PathDraw({ labels }: { labels: Record<string, string> }) {
  const [run, setRun] = useState(0)
  const svg = useRef<SVGSVGElement>(null)

  useLayoutEffect(() => {
    svg.current?.querySelectorAll('path').forEach((p) => p.style.setProperty('--len', String(Math.ceil(p.getTotalLength()))))
  }, [run])

  return (
    <div className={s.wrap}>
      <svg key={run} ref={svg} className={s.svg} viewBox="0 0 280 150" role="img" aria-label={labels.label}>
        <path className={s.grid} style={{ '--d': 0 } as React.CSSProperties} d="M20 135 H260" />
        <path className={s.house} style={{ '--d': 1 } as React.CSSProperties} d="M8 120 V100 L20 90 L32 100 V120 Z" />
        <path className={s.studio} style={{ '--d': 2 } as React.CSSProperties} d="M248 30 V10 H272 V30 Z M254 18 H266" />
        <path className={s.route} style={{ '--d': 3 } as React.CSSProperties} d={ROUTE} />
        <circle className={s.dot} r="5" style={{ offsetPath: `path("${ROUTE}")` }} />
      </svg>
      <button type="button" className={s.button} onClick={() => setRun(run + 1)}>
        {labels.replay}
      </button>
    </div>
  )
}
demo.module.css
.wrap { display: grid; justify-items: center; gap: 12px; width: min(300px, 100%); }

.svg { width: 100%; height: auto; overflow: visible; }

.svg path {
  fill: none;
  stroke: var(--stage-ink);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--len, 1000);
  stroke-dashoffset: var(--len, 1000);
  animation: draw 1.6s var(--ease-in-out) forwards;
  animation-delay: calc(var(--d) * 250ms);
}

.svg .grid { stroke: var(--stage-rule); }
.svg .route { stroke: var(--stage-accent); stroke-width: 3; stroke-dasharray: var(--len, 1000); }

.dot {
  fill: var(--stage-ink);
  offset-distance: 0%;
  opacity: 0;
  animation: ride 1.8s 2.2s var(--ease-in-out) forwards;
}

@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes ride {
  0% { offset-distance: 0%; opacity: 1; }
  100% { offset-distance: 100%; opacity: 1; }
}

.button {
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: 8px;
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.button:hover { border-color: var(--stage-accent); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .svg path { animation: none; stroke-dashoffset: 0; }
  .dot { animation: none; opacity: 1; offset-distance: 100%; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.