Stitch Path
loomui/stitch-pathFreeComponent
A running stitch sewn along an SVG path as the page scrolls, following the holes it is laid over.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/stitch-path.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface StitchPathProps extends Omit<8 React.ComponentProps<"svg">,9 "target"10> {11 /** Path data, in the `0 0 100 100` box the component draws in. */12 d?: string13 /** Length of one stitch. */14 stitch?: number15 /** Bare thread between two stitches. */16 gap?: number17 /** Thread thickness in pixels. It does not stretch with the box. */18 thickness?: number19 /** Sew it yourself, 0 to 1. Leave it out to sew on scroll. */20 progress?: number21 /** Follow this element through the viewport instead of the path's own box. */22 target?: React.RefObject<HTMLElement | null>23 /** Draw the holes the thread is sewn through. */24 guide?: boolean25}2627const DEFAULT_PATH = "M 0 62 C 18 6 42 6 52 48 S 78 96 100 34"2829const clamp = (value: number) => Math.min(Math.max(value, 0), 1)3031export function StitchPath({32 d = DEFAULT_PATH,33 stitch = 5,34 gap = 4,35 thickness = 2,36 progress,37 target,38 guide = true,39 className,40 ...props41}: StitchPathProps) {42 const maskId = React.useId()43 const hostRef = React.useRef<SVGSVGElement>(null)44 const revealRef = React.useRef<SVGPathElement>(null)45 const controlled = progress !== undefined4647 React.useEffect(() => {48 const node = revealRef.current49 if (!node) {50 return51 }5253 // `pathLength="1"` normalises the path, so the offset is the fraction of it54 // still to be sewn and the geometry never has to be measured.55 const draw = (value: number) => {56 node.style.strokeDashoffset = `${1 - clamp(value)}`57 }5859 if (controlled) {60 draw(progress)61 return62 }6364 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {65 draw(1)66 return67 }6869 let frame = 07071 const measure = () => {72 const box = target?.current ?? hostRef.current73 if (!box) {74 return75 }7677 // Zero when the top edge sits on the bottom of the viewport, one when the78 // bottom edge has cleared the top of it.79 const rect = box.getBoundingClientRect()80 const travel = window.innerHeight + rect.height81 draw(travel > 0 ? (window.innerHeight - rect.top) / travel : 1)82 }8384 const onScroll = () => {85 cancelAnimationFrame(frame)86 frame = requestAnimationFrame(measure)87 }8889 measure()90 window.addEventListener("scroll", onScroll, { passive: true })91 window.addEventListener("resize", onScroll, { passive: true })92 return () => {93// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep |
