Vanilla HTML/CSS/JS component — stroke <path> in SVG "draws" progressively as if a hand is drawing it, based on % scrolled through the viewport or once when entering the viewport. No external libraries (no Anime.js, no GSAP).
WHAT
Purpose and context
- Purpose: stroke
<path>in SVG "draws" progressively based on % scroll or once when entering the viewport (stroke-dasharray/dashoffset), vanilla with no external libs; by default use the original verbatim version atRheinmir/uiux-asset. - Trigger (when to use):
- Logo/signature/icon needs a "progressive draw" effect when entering the viewport (runs once).
- Line charts, journey diagrams, route lines need to "draw out" continuously according to scroll progress (tied to actual scroll position, not run once).
- User says "draw svg on scroll", "svg stroke draw", "logo progressive draw", "path animation reveal", "stroke-dashoffset", or /svg-stroke-reveal.
- Non-goals: DO NOT use for the 7 general scroll effects (opacity/translate reveal, stagger text, highlight, nav shrink, scroll-to-top, scrollspy, parallax) — that is
skills/scroll-effects. Do not use for<text>not yet converted to outline (see Rules).
Mental model
[data-svgr] > <path>×N → getTotalLength() → dasharray = dashoffset = length (ẩn) → mode "once" (IO một lần + CSS transition) hoặc mode "scroll" (IO bật cờ + 1 listener rAF ghi dashoffset theo progress) → reduce-motion: dashoffset = 0 ngay.
Input and output contract
| Field | Required? | Meaning | |
|---|---|---|---|
| In | The project's real SVG (the <path> with strokes) |
yes | <text> must be converted to outline first |
| In | mode once or scroll |
yes | logo/signature → once; chart/journey → scroll |
| In | design.md of the project |
no | if present, change the color token according to it |
| Out | SVG with [data-svgr] + CSS/JS attached in the project |
yes | copy the original file verbatim or adapt assets/ |
| Out | verification evidence playwright-verify |
yes | before/during/after images (scroll) + once only triggers 1 time |
Rules and capabilities
- RULE-01 (MUST):
getTotalLength()measured on the path's intrinsic coordinates, NOT affected by CSS transform — but if you scale the element containing the path withtransform: scale(sx, sy)withsx != sy(NON-UNIFORM scale on two axes) AFTER setting dasharray, the drawing speed in px will be skewed between the two axes because the animation still runs according to the original length while the display has been proportionally distorted. Always set size viaviewBox+width/heightuniformly proportioned, avoid axis-skewed CSS scale on the element carrying the path itself. - RULE-02 (MUST):
<text>cannot stroke-draw — must convert text/font to outline<path>first (Illustrator "Create Outlines", Figma "Flatten", or a font-to-path conversion tool), then apply[data-svgr]. Applying directly to<text>will do nothing because<text>has nogetTotalLength(). - RULE-03 (MUST): Mode "once" uses CSS transition (smooth, low CPU cost); mode "scroll" sets dashoffset directly every frame via JS (required to stay tightly in sync with scroll position) — do not swap these two approaches.
- RULE-04 (MUST): One
IntersectionObserverseparate for each mode (do not merge once/scroll logic into 1 observer because behaviors differ: unobserve-once vs continuously toggle flag) and one single rAF-throttled scroll listener for all SVG mode "scroll" on the page — do not attach multiple separatescrolllisteners for each SVG. - RULE-05 (MUST): Verify with
playwright-verify: screenshot before/during/after scrolling through SVG mode "scroll" to see dashoffset decrease gradually, and assert SVG mode "once" triggers only 1 time even if scrolled in and out multiple times. - Capabilities: read the original (web/git) + asset skill +
design.md; write HTML/CSS/JS into the target project; run a headless browser to verify.
Failure boundaries
- Apply to
<text>not yet converted to outline → nothing happens: blocked, require conversion to<path>first (RULE-02). - Element carrying the path is skewed off-axis by CSS scale → drawing speed distorted: failed, move sizing to
viewBox+width/height(RULE-01). - Verify assert red (dashoffset not decreasing gradually, once triggers again) → failed, fix then verify again.
HOW
Main workflow
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | SVG + requirements | Choose mode once or scroll; check whether the SVG is already <path> (no <text>) |
mode | <text> → blocked until converted |
| W02 | effect | original Pen table | Open link run, copy the original file .html (+ _assets/) into the project, keep the attribution comment |
original file in the project | must not pull in external libs → B01 |
| W03 | effect | file + actual SVG + design.md |
Only edit content/images/colors (replace path d=) |
adapted SVG | — |
| W04 | deterministic | page running over HTTP | Verify with playwright-verify (RULE-05) |
images + asserts | red → fix, repeat W04 |
Branches
| ID | Kind | Guard | Behavior | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | conditional_required | the project must NOT pull external libs (original version) | use the trimmed vanilla version assets/ (Reference section — Assets), read then ADAPT |
— | W04 |
| B02 | conditional_required | prefers-reduced-motion: reduce |
strokeDashoffset = 0 right at init, remove observer/listener |
— | W04 |
Validation and stopping
Deterministic assert at W04 (scroll mode: dashoffset decreases across before/mid/after images; once mode: triggers only once even when scrolling in and out). Needs eyes: smoothness, stroke order. Stop when asserts are green.
Examples
- Positive: "logo draws gradually when entering the screen" →
data-svgr="once"on a logo with 4 paths → when 30% enters viewport, draw sequentially (stagger 160ms in DOM order), scrolling out and back in does not redraw. - Boundary/failure: the signature is
<text>script font → nogetTotalLength(), nothing draws → stop, request converting to outline (Figma "Flatten") into<path>then apply[data-svgr].
Reference — Original — use FIRST (copy the file as-is, do not rewrite)
The author's VERBATIM original code (public CodePen = MIT) is in the external repo Rheinmir/uiux-asset — live gallery: https://rheinmir.github.io/uiux-asset/scroll-effects/ .
Process: open link run to see the exact effect → copy the file as-is .html (+ folder _assets/ if the file references it) into the project → keep the attribution comment on the first line → only change content/images/colors. Run locally over HTTP, not file://.
git clone --depth 1 https://github.com/Rheinmir/uiux-asset if the full offline set is needed.
| Original Pen — author | Link |
|---|---|
| Interactive Cicada Genomics Landing Page — alyona-mysiura | run · code |
| Scroll-Driven SVG Map Editorial Gallery — knyttneve | run · code |
| Scroll-Driven Godzilla Walk-and-Destroy Animation — creativeocean | run · code |
assets/ in this skill is only a self-written trimmed vanilla version — use when the project must NOT pull external libs; by default use the original version above.
Reference — Assets
assets/demo.html + assets/svg-stroke-reveal.css + assets/svg-stroke-reveal.js — read then ADAPT into the project's actual SVG (replace path d=, change color tokens per design.md if any), do not copy mechanically.
Reference — Mechanism
- For EACH
<path>in[data-svgr]: JS callspath.getTotalLength(), set inlinestyle.strokeDasharray = lengthandstyle.strokeDashoffset = lengthat init → stroke fully hidden (dash exactly equal to path length, offset pushes the entire dash outside). - Mode
data-svgr="once"(one-time threshold — logo/signature):IntersectionObserverthreshold 0.3, when entering viewport → setstrokeDashoffset = 0for all child paths → CSStransition: stroke-dashoffsetrun once →unobserveimmediately after trigger, no repeat even if scrolled out and back in. - Mode
data-svgr="scroll"(continuous progress — chart/journey):IntersectionObserveronly toggles the "in viewport" flag; while it is on, a rAF-throttled scroll listener (shared single listener, not multiple) computesprogress = (innerHeight - rect.top) / (innerHeight + rect.height)clamped 0..1, then writes directlystrokeDashoffset = length * (1 - progress)every frame — do NOT use CSS transition in this mode (transition will make the stroke lag behind the actual scroll position). - Complex path with many
<path>children (multi-stroke logo, signature) — "once" mode staggers viatransition-delay: calc(var(--svgr-i) * 160ms), JS assigns--svgr-iin the ORDER of paths in the DOM (first path draws first). prefers-reduced-motion: reduce→ JS setsstrokeDashoffset = 0immediately for all paths at init, bypassing both observer/listener — shows full stroke instantly, no animation.