All examples / scroll story
Route drawing: a line that draws itself as you scroll
A small isometric map with three harbour towns; as the map crosses the view, a coral route draws itself from town to town and each town's pin pops up as the line reaches it. CSS view timeline and stroke-dashoffset, no script.
Prompt tested
Day 2 · The coast road
Three harbour towns, one easy drive
Brindlecove plans the stops and the breaks. You just follow the line.
Lunch in Orlow Bay, the ferry at four, and the evening in Tarn End.
Prompt
Build a scroll story section in which a route on a small isometric map draws itself as the map scrolls through the view: a CSS scroll-driven animation on a named view timeline (view-timeline-name on the map figure, animation-timeline on the route and pins; no JavaScript, no scroll event listeners, no libraries), as one HTML fragment plus CSS scoped under one wrapper class. Layout, all centred on a paper background #F6F4EF with ink text #1F2A44: an intro block at least 80cqh tall with its text centred vertically (a small uppercase slate #5B6B8C kicker 'Day 2 · The coast road', an h2 'Three harbour towns, one easy drive', one short line of copy for an invented trip planner called Brindlecove); then a figure (max-width 44rem) holding one inline SVG map (viewBox 400×260, width 100%, max-height 70cqh) with a slate caption naming the route and its distance; then a closing line in a block at least 70cqh tall, centred. Use cqh for those heights, never vh. The map: one isometric tile (a 30° diamond with a paper top, a darker left side and a lighter right side, navy #1F2A44 1.5 px outlines, round joins), a pale sky-blue lake, a pale mint field, a few small two-tone mint cone trees and three towns of two or three tiny isometric houses (pale grey walls, coral pyramid roofs, thin navy outlines), labelled in small bold text: Fennick on the left, Orlow Bay at the top middle, Tarn End at the lower right. A wide pale road (#E8ECF3) curves from Fennick up to Orlow Bay and down to Tarn End; on top of it the same path is drawn again as a 3.5 px coral (#FF8A7A) route with round caps, pathLength="1" and stroke-dasharray: 1. Each town has a map pin (a teardrop with a paper dot, navy outline): mint, sun yellow #FFC861 and coral. Motion, on the figure's view timeline: the route's stroke-dashoffset goes from 1 to 0 over animation-range: cover 20% cover 70%, linear; each pin pops up (scale 0 to 1.15 to 1 with opacity 0 to 1, transform-box: fill-box, transform-origin at its bottom tip) over a short range just as the line reaches its town: about cover 17%–23%, 45%–51% and 67%–73% (Orlow Bay sits 55% along the line). Every animation uses animation-duration: auto and animation-fill-mode: both. The base styles are the finished map (route fully drawn, pins up); put only the animation lines and view-timeline-name inside @supports (animation-timeline: view()), so other browsers show the finished map, and add a prefers-reduced-motion rule with animation: none that shows the same finished map. No position: fixed and no overflow: hidden or auto.
Words it rests on
- scrubbed SVG line drawing
- stroke-dasharray / stroke-dashoffset with pathLength="1"
- CSS scroll-driven animation on a named view timeline (view-timeline-name, animation-timeline)
- animation-range: cover 20% cover 70%
- staggered animation-range per pin, timed to where the line reaches each town
- transform-box: fill-box; transform-origin at the pin's tip
- animation-duration: auto + animation-fill-mode: both
- @supports (animation-timeline: view()) holding only the animation lines
- the base styles are the finished map
- isometric map tile (30° projection)
- heights in cqh, not vh
- no scroll event listeners
Prompt test
A fresh AI session given only this prompt rebuilt this on 2026-11-05 using claude-opus-5-5; a separate fresh session using claude-opus-5-5 judged it “like this”.


Lesson: For a scrubbed line drawing, naming `pathLength="1"` with `stroke-dasharray: 1`, the figure's named view timeline, the exact `animation-range` for the line and for each pin (with where the middle town sits along the line) got a rebuild whose line and pins move over the same slices of the scroll at both widths, on the first try. The map's furniture (lake, field, tree style, where the route bends) was described loosely and drifted; name positions when the picture matters.
Facts
- Tags
- scroll story view timeline stroke dashoffset isometric flat shaded harbor
- Weight
- 2.1 KB gzip (HTML + CSS)
- Needs
- No library, no JavaScript
- Browser support
- CSS scroll-driven animations (view-timeline, animation-range): Chrome and Edge 115+, Safari 26+; Firefox only behind a flag (MDN). Other browsers show the finished layout: the route drawn and every pin up.
- Motion
- follows the scroll; without support or with reduced motion the finished layout shows
- Link
/examples/scroll-draw/
Source
Paste both parts into any page. Wrap the HTML in <div class="ma-scroll-draw"> and put the CSS in a <style> element or your stylesheet; every class, id and animation name starts with ma-scroll-draw, so it will not clash with your styles.
demo.html
<div class="ma-scroll-draw-story">
<div class="ma-scroll-draw-intro">
<p class="ma-scroll-draw-kicker">Day 2 · The coast road</p>
<h2>Three harbour towns, one easy drive</h2>
<p>Brindlecove plans the stops and the breaks. You just follow the line.</p>
</div>
<figure class="ma-scroll-draw-map">
<svg viewBox="0 0 400 260" role="img" aria-labelledby="ma-scroll-draw-title">
<title id="ma-scroll-draw-title">Map: the route runs from Fennick past Orlow Bay to Tarn End</title>
<defs>
<g id="ma-scroll-draw-house">
<path d="M-8 -4 L0 0 L0 -9 L-8 -13 Z" fill="#C9D2E3"/>
<path d="M0 0 L8 -4 L8 -13 L0 -9 Z" fill="#E8ECF3"/>
<path d="M-8 -13 L0 -9 L0 -21 Z" fill="#E06A5B"/>
<path d="M0 -9 L8 -13 L0 -21 Z" fill="#FF8A7A"/>
</g>
<g id="ma-scroll-draw-tree">
<path d="M0 0 L-5 -3 L0 -15 Z" fill="#4FB896"/>
<path d="M0 0 L5 -3 L0 -15 Z" fill="#6FD6B4"/>
</g>
<path id="ma-scroll-draw-pin-shape" d="M0 0 C-2 -5 -7 -9 -7 -14 A7 7 0 1 1 7 -14 C7 -9 2 -5 0 0 Z"/>
</defs>
<g stroke="#1F2A44" stroke-width="1.5" stroke-linejoin="round">
<path d="M30 128 L200 226 L200 238 L30 140 Z" fill="#B7C1D3"/>
<path d="M200 226 L370 128 L370 140 L200 238 Z" fill="#E8ECF3"/>
<path d="M200 30 L370 128 L200 226 L30 128 Z" fill="#F6F4EF"/>
</g>
<ellipse cx="160" cy="168" rx="34" ry="15" fill="#7FB6FF" opacity="0.55"/>
<path d="M268 88 L298 105 L278 116 L248 99 Z" fill="#A8E8D2"/>
<path class="ma-scroll-draw-road" d="M100 128 C130 95 175 75 215 90 C255 105 250 150 290 160" fill="none" stroke="#E8ECF3" stroke-width="9" stroke-linecap="round"/>
<path class="ma-scroll-draw-route" d="M100 128 C130 95 175 75 215 90 C255 105 250 150 290 160" pathLength="1" fill="none" stroke="#FF8A7A" stroke-width="3.5" stroke-linecap="round"/>
<g stroke="#1F2A44" stroke-width="1" stroke-linejoin="round">
<use href="#ma-scroll-draw-house" x="84" y="140"/>
<use href="#ma-scroll-draw-house" x="100" y="148"/>
<use href="#ma-scroll-draw-house" x="200" y="104"/>
<use href="#ma-scroll-draw-house" x="230" y="100"/>
<use href="#ma-scroll-draw-house" x="215" y="112"/>
<use href="#ma-scroll-draw-house" x="300" y="172"/>
<use href="#ma-scroll-draw-house" x="282" y="178"/>
<use href="#ma-scroll-draw-tree" x="64" y="130"/>
<use href="#ma-scroll-draw-tree" x="150" y="118"/>
<use href="#ma-scroll-draw-tree" x="162" y="124"/>
<use href="#ma-scroll-draw-tree" x="250" y="70"/>
<use href="#ma-scroll-draw-tree" x="318" y="132"/>
<use href="#ma-scroll-draw-tree" x="236" y="196"/>
</g>
<g stroke="#1F2A44" stroke-width="1.5" stroke-linejoin="round">
<g transform="translate(100 128)"><g class="ma-scroll-draw-pin ma-scroll-draw-pin-1"><use href="#ma-scroll-draw-pin-shape" fill="#6FD6B4"/><circle cy="-14" r="2.6" fill="#F6F4EF"/></g></g>
<g transform="translate(215 90)"><g class="ma-scroll-draw-pin ma-scroll-draw-pin-2"><use href="#ma-scroll-draw-pin-shape" fill="#FFC861"/><circle cy="-14" r="2.6" fill="#F6F4EF"/></g></g>
<g transform="translate(290 160)"><g class="ma-scroll-draw-pin ma-scroll-draw-pin-3"><use href="#ma-scroll-draw-pin-shape" fill="#FF8A7A"/><circle cy="-14" r="2.6" fill="#F6F4EF"/></g></g>
</g>
<g class="ma-scroll-draw-labels" fill="#1F2A44" font-size="11" font-weight="600" text-anchor="middle">
<text x="134" y="152">Fennick</text>
<text x="215" y="132">Orlow Bay</text>
<text x="318" y="196">Tarn End</text>
</g>
</svg>
<figcaption>Fennick → Orlow Bay → Tarn End · 64 km, two stops</figcaption>
</figure>
<div class="ma-scroll-draw-outro">
<p>Lunch in Orlow Bay, the ferry at four, and the evening in Tarn End.</p>
</div>
</div>
demo.css
/* Scrubbed line drawing: a route on a small isometric map draws itself as the map crosses the
view, and each town's pin pops up as the line reaches it. The figure names a view timeline;
the route and the pins run on it, each over its own slice of the cover range. The base styles
are the finished map (route drawn, pins up), so a browser without scroll timelines, reduced
motion and pause all show it complete. */
.ma-scroll-draw .ma-scroll-draw-story {
background: #F6F4EF;
color: #1F2A44;
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
padding: 0 1.25rem;
}
.ma-scroll-draw .ma-scroll-draw-intro,
.ma-scroll-draw .ma-scroll-draw-outro {
display: flex;
flex-direction: column;
justify-content: center;
max-width: 34rem;
margin: 0 auto;
text-align: center;
}
.ma-scroll-draw .ma-scroll-draw-intro {
min-height: 80cqh;
}
.ma-scroll-draw .ma-scroll-draw-outro {
min-height: 70cqh;
font-size: 1.1rem;
color: #5B6B8C;
}
.ma-scroll-draw .ma-scroll-draw-kicker {
margin: 0 0 0.5rem;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #5B6B8C;
}
.ma-scroll-draw h2 {
margin: 0 0 0.75rem;
font-size: 1.75rem;
line-height: 1.2;
}
.ma-scroll-draw p {
margin: 0;
line-height: 1.6;
}
.ma-scroll-draw .ma-scroll-draw-map {
max-width: 44rem;
margin: 0 auto;
}
.ma-scroll-draw .ma-scroll-draw-map svg {
display: block;
width: 100%;
height: auto;
max-height: 70cqh;
margin: 0 auto;
}
.ma-scroll-draw .ma-scroll-draw-map figcaption {
margin-top: 0.5rem;
font-size: 0.9rem;
text-align: center;
color: #5B6B8C;
}
.ma-scroll-draw .ma-scroll-draw-labels {
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
}
/* pathLength="1" makes one dash exactly as long as the route. */
.ma-scroll-draw .ma-scroll-draw-route {
stroke-dasharray: 1;
stroke-dashoffset: 0;
}
.ma-scroll-draw .ma-scroll-draw-pin {
transform-box: fill-box;
transform-origin: 50% 100%;
}
@keyframes ma-scroll-draw-line {
from { stroke-dashoffset: 1; }
to { stroke-dashoffset: 0; }
}
@keyframes ma-scroll-draw-pop {
0% { transform: scale(0); opacity: 0; }
70% { transform: scale(1.15); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@supports (animation-timeline: view()) {
.ma-scroll-draw .ma-scroll-draw-map {
view-timeline-name: --ma-scroll-draw-map;
}
.ma-scroll-draw .ma-scroll-draw-route {
animation-name: ma-scroll-draw-line;
animation-duration: auto;
animation-timing-function: linear;
animation-fill-mode: both;
animation-timeline: --ma-scroll-draw-map;
animation-range: cover 20% cover 70%;
}
.ma-scroll-draw .ma-scroll-draw-pin {
animation-name: ma-scroll-draw-pop;
animation-duration: auto;
animation-timing-function: ease-out;
animation-fill-mode: both;
animation-timeline: --ma-scroll-draw-map;
}
/* Each pin pops as the line reaches its town. */
.ma-scroll-draw .ma-scroll-draw-pin-1 {
animation-range: cover 17% cover 23%;
}
.ma-scroll-draw .ma-scroll-draw-pin-2 {
animation-range: cover 45% cover 51%;
}
.ma-scroll-draw .ma-scroll-draw-pin-3 {
animation-range: cover 67% cover 73%;
}
}
@media (prefers-reduced-motion: reduce) {
.ma-scroll-draw,
.ma-scroll-draw * {
animation: none !important;
}
}