Scroll Draw Path

A route line draws itself as you scroll and rewinds when you scroll back, tying progress to the reader’s hand.

  • JS
  • SVG & shapes
  • Scroll
  • Added
Scroll ↓

AI prompt

Create an SVG path that draws in step with scroll, in React. Give the path pathLength="1" so stroke-dasharray: 1 and stroke-dashoffset: 1 hide it fully regardless of its real length. On scroll, compute progress 0 to 1 through the section and write it into a CSS custom property --p; set stroke-dashoffset: calc(1 - var(--p)) on the path so it draws forward on scroll down and rewinds on scroll up. Keep the SVG in a sticky container while the section scrolls. Draw a faint copy of the path underneath as the track, and add a dot that fills when progress reaches 1. Under prefers-reduced-motion, show the full path. The endpoints read "Start" and "Finish".

Source

Demo.tsx
'use client'

import s from './demo.module.css'

const D = 'M20 150 C 70 150, 60 60, 120 70 S 170 150, 220 110 S 270 30, 300 40'

export default function ScrollDraw({ labels }: { labels: Record<string, string> }) {
  const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
    const el = e.currentTarget
    const p = el.scrollTop / (el.scrollHeight - el.clientHeight || 1)
    el.style.setProperty('--p', String(p))
    el.toggleAttribute('data-done', p > 0.99)
  }

  return (
    <div className={s.frame} tabIndex={0} onScroll={onScroll}>
      <div className={s.section}>
        <div className={s.sticky}>
          <svg viewBox="0 0 320 180" className={s.svg} aria-hidden="true">
            <path d={D} className={s.track} />
            <path d={D} className={s.path} pathLength={1} />
            <circle cx="20" cy="150" r="5" className={s.startDot} />
            <circle cx="300" cy="40" r="6" className={s.endDot} />
            <text x="20" y="172" className={s.text}>
              {labels.start}
            </text>
            <text x="300" y="22" className={s.text} textAnchor="end">
              {labels.end}
            </text>
          </svg>
          <span className={s.label}>{labels.hint} ↓</span>
        </div>
      </div>
    </div>
  )
}
demo.module.css
.frame {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }

.hint {
  display: block;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--stage-muted);
}

.frame { --p: 0; }

.section { height: 300%; }

.sticky {
  position: sticky;
  top: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 4px;
  height: 240px;
}

.svg { width: min(300px, 90%); overflow: visible; }

.track, .path {
  fill: none;
  stroke-width: 3;
  stroke-linecap: round;
}

.track { stroke: var(--stage-rule); stroke-dasharray: 2 8; }

.path {
  stroke: var(--stage-accent);
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p));
}

.startDot { fill: var(--stage-accent); }

.endDot {
  fill: var(--stage-bg);
  stroke: var(--stage-accent);
  stroke-width: 2;
  transition: fill var(--dur-short);
}

.frame[data-done] .endDot { fill: var(--stage-accent); }

.text { fill: var(--stage-muted); font: 500 11px var(--font-mono); }

.label {
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stage-muted);
}

@media (prefers-reduced-motion: reduce) {
  .path { stroke-dashoffset: 0; }
}

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.