Handwritten Notes
Testimonial notes scattered on a cutting mat: drag cards with spring physics and click to bring one forward.
- Testimonials
- Education
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers (Pointer Events). On narrow screens notes stack and drag sideways so the page still scrolls.
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/handwritten-notes.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/testimonials/handwritten-notes.tsx
1/**2 * HandwrittenNotes, student testimonials as paper notes scattered on a3 * cutting mat: index cards, sticky notes, a kraft card, a page of graph paper4 * and a postcard, some with a taped photo. Drag any note to rearrange it5 * (pointer events + a small spring integrator), click or focus brings it to6 * the front, "Put them back" springs everything home.7 *8 * Props: notes (Note[]), eyebrow, title, stats ([value, label][]), className.9 * Note: { id, kind, text, name, meta, photo?, x, y, rot, w }10 * kind: "index" | "sticky" | "pink" | "kraft" | "graph" | "postcard"11 * x (% of desk width), y (px from desk top), rot (deg), w (px). Desktop12 * only; under 760px notes stack in a loose column and drag sideways13 * (vertical swipes keep scrolling the page).14 *15 * Handwriting: a cursive system stack ("Bradley Hand", "Segoe Print",16 * "Marker Felt") is used only for short sticky notes and signatures; longer17 * notes are set in a readable serif.18 * Browser support: all evergreen browsers (Pointer Events).19 * Accessibility: each note is a focusable <figure> with an explicit label;20 * arrow keys move the focused note (Shift = bigger steps), Enter brings it to21 * the front. Reduced motion: no spring or tilt, notes move 1:1.22 */23import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from "react";24 25export type NoteKind = "index" | "sticky" | "pink" | "kraft" | "graph" | "postcard";26export interface Note { id: string; kind: NoteKind; text: string; name: string; meta: string; photo?: string; x: number; y: number; rot: number; w: number }27 28export interface HandwrittenNotesProps {29 notes?: Note[];30 eyebrow?: string;31 title?: string;32 stats?: [string, string][];33 className?: string;34}35 36const P = "/img/people/";37 38export const DEFAULT_NOTES: Note[] = [39 { id: "marta", kind: "index", x: 1.5, y: 36, rot: -3, w: 280, name: "Marta", meta: "Lisbon · cohort 14",40 text: "I hadn’t drawn since I was eleven. On day one my mug looked like a bucket. On day thirty I drew my dad’s hands and he put it on the fridge." },41 { id: "ben", kind: "sticky", x: 27, y: 4, rot: 4, w: 196, name: "Ben", meta: "cohort 12",42 text: "Ten minutes a day. That’s it. That’s the whole trick." },43 { id: "hyunwoo", kind: "kraft", x: 46, y: 58, rot: -2, w: 276, photo: P + "p8.webp", name: "Hyun-woo", meta: "Seoul · cohort 15",44 text: "Perspective week nearly broke me. Tomoko’s “draw the box, then lie about it” video fixed it in six minutes." },45 { id: "priya", kind: "graph", x: 74, y: 16, rot: 3, w: 262, photo: P + "p6.webp", name: "Priya", meta: "Leeds · cohort 13",46 text: "My sketchbook from March has 61 pages. My sketchbooks from the five years before that have 9 pages, combined." },47 { id: "kofi", kind: "pink", x: 4, y: 400, rot: 5, w: 236, photo: P + "p4.webp", name: "Kofi", meta: "cohort 14",48 text: "Drew on the 7:42 train every day. A stranger asked to buy one." },49 { id: "grace", kind: "postcard", x: 29, y: 372, rot: -3.5, w: 360, photo: P + "p9.webp", name: "Grace", meta: "Cape Town · cohort 11",50 text: "Dear Tomoko, I was the one who kept apologising in the feedback threads. I’ve stopped. Thank you for being so kind about my terrible ellipses." },51 { id: "lukas", kind: "index", x: 67, y: 420, rot: 2, w: 290, name: "Lukas", meta: "Graz · cohort 15",52 text: "Worth it for watercolour week alone. I finally understand why every green I mixed for ten years turned into mud." },53];54 55type Body = { x: number; y: number; vx: number; vy: number; tx: number; ty: number; r: number; vr: number; s: number; vs: number; drag: boolean };56 57const K = 380, C = 30;58 59export function HandwrittenNotes({60 notes = DEFAULT_NOTES,61 eyebrow = "Line & Wash · a 30-day sketching course",62 title = "Day thirty. Here’s what they left on the desk.",63 stats = [["2,140", "students since 2023"], ["86%", "finish all 30 days"], ["10 min", "a day, honestly"]],64 className,65}: HandwrittenNotesProps) {66 const desk = useRef<HTMLDivElement>(null);67 const els = useRef(new Map<string, HTMLElement>());68 const bodies = useRef(new Map<string, Body>());69 const raf = useRef(0);70 const reduced = useRef(false);71 const [order, setOrder] = useState(() => notes.map((n) => n.id));72 const [moved, setMoved] = useState(false);73 const drag = useRef<{ id: string; px: number; py: number; ox: number; oy: number; lx: number; lt: number; vx: number } | null>(null);74 75 const body = (id: string) => {76 let b = bodies.current.get(id);77 if (!b) { b = { x: 0, y: 0, vx: 0, vy: 0, tx: 0, ty: 0, r: 0, vr: 0, s: 1, vs: 0, drag: false }; bodies.current.set(id, b); }78 return b;79 };80 const rest = (id: string) => notes.find((n) => n.id === id)?.rot ?? 0;81 82 const paint = (id: string) => {83 const el = els.current.get(id);84 const b = bodies.current.get(id);85 if (el && b) el.style.transform = `translate3d(${b.x.toFixed(2)}px,${b.y.toFixed(2)}px,0) rotate(${(rest(id) + b.r).toFixed(2)}deg) scale(${b.s.toFixed(4)})`;86 };87 88 const kick = () => {89 if (raf.current) return;90 let last = performance.now();91 const step = (t: number) => {92 const dt = Math.min(1 / 30, (t - last) / 1000);93 last = t;94 let busy = false;95 bodies.current.forEach((b, id) => {96 const tr = b.drag ? Math.max(-14, Math.min(14, (drag.current?.vx ?? 0) * 0.018)) : 0;97 const ts = b.drag ? 1.045 : 1;98 if (reduced.current) { b.x = b.tx; b.y = b.ty; b.r = 0; b.s = 1; b.vx = b.vy = b.vr = b.vs = 0; paint(id); return; }99 b.vx += (-K * (b.x - b.tx) - C * b.vx) * dt; b.x += b.vx * dt;100 b.vy += (-K * (b.y - b.ty) - C * b.vy) * dt; b.y += b.vy * dt;101 b.vr += (-K * 0.6 * (b.r - tr) - C * 0.7 * b.vr) * dt; b.r += b.vr * dt;102 b.vs += (-K * (b.s - ts) - C * b.vs) * dt; b.s += b.vs * dt;103 const still = Math.abs(b.x - b.tx) < 0.1 && Math.abs(b.y - b.ty) < 0.1 && Math.abs(b.vx) + Math.abs(b.vy) < 0.5 && Math.abs(b.r - tr) < 0.05 && Math.abs(b.s - ts) < 0.0005 && Math.abs(b.vr) < 0.1;104 if (still && !b.drag) { b.x = b.tx; b.y = b.ty; b.r = 0; b.s = 1; }105 else busy = true;106 paint(id);107 });108 raf.current = busy ? requestAnimationFrame(step) : 0;109 };110 raf.current = requestAnimationFrame(step);111 };112 113 useEffect(() => {114 reduced.current = matchMedia("(prefers-reduced-motion: reduce)").matches;115 return () => cancelAnimationFrame(raf.current);116 }, []);117 118 const front = (id: string) => setOrder((o) => (o[o.length - 1] === id ? o : [...o.filter((x) => x !== id), id]));119 120 const clampTarget = (id: string, b: Body) => {121 const d = desk.current, el = els.current.get(id);122 if (!d || !el) return;123 const dr = d.getBoundingClientRect();124 const left = el.offsetLeft, top = el.offsetTop, w = el.offsetWidth, h = el.offsetHeight;125 const m = 40;126 b.tx = Math.max(-left - w + m + 60, Math.min(dr.width - left - m - 20, b.tx));127 b.ty = Math.max(-top - 20, Math.min(dr.height - top - m, b.ty));128 };129 130 const onDown = (id: string) => (e: PointerEvent<HTMLElement>) => {131 if (e.button !== 0) return;132 front(id);133 const b = body(id);134 b.drag = true;135 drag.current = { id, px: e.clientX, py: e.clientY, ox: b.tx, oy: b.ty, lx: e.clientX, lt: performance.now(), vx: 0 };136 e.currentTarget.setPointerCapture(e.pointerId);137 kick();138 };139 const onMove = (id: string) => (e: PointerEvent<HTMLElement>) => {140 const d = drag.current;141 if (!d || d.id !== id) return;142 const b = body(id);143 b.tx = d.ox + e.clientX - d.px;144 b.ty = d.oy + e.clientY - d.py;145 clampTarget(id, b);146 const now = performance.now();147 const inst = ((e.clientX - d.lx) / Math.max(1, now - d.lt)) * 1000;148 d.vx = d.vx * 0.7 + inst * 0.3;149 d.lx = e.clientX; d.lt = now;150 if (Math.abs(b.tx) + Math.abs(b.ty) > 4) setMoved(true);151 kick();152 };153 const onUp = (id: string) => () => {154 const b = bodies.current.get(id);155 if (b) b.drag = false;156 drag.current = null;157 kick();158 };159 const onKey = (id: string) => (e: KeyboardEvent<HTMLElement>) => {160 const step = e.shiftKey ? 64 : 16;161 const b = body(id);162 if (e.key === "ArrowLeft") b.tx -= step;163 else if (e.key === "ArrowRight") b.tx += step;164 else if (e.key === "ArrowUp") b.ty -= step;165 else if (e.key === "ArrowDown") b.ty += step;166 else if (e.key === "Enter" || e.key === " ") { front(id); e.preventDefault(); return; }167 else return;168 e.preventDefault();169 clampTarget(id, b);170 front(id);171 setMoved(true);172 kick();173 };174 const putBack = () => {175 bodies.current.forEach((b) => { b.tx = 0; b.ty = 0; });176 setOrder(notes.map((n) => n.id));177 setMoved(false);178 kick();179 };180 181 return (182 <section className={`fd-hwn${className ? " " + className : ""}`} aria-label="Student notes">183 <style>{css}</style>184 <div className="fd-hwn-head">185 <div>186 <p className="fd-hwn-eyebrow">{eyebrow}</p>187 <h2 className="fd-hwn-title">{title}</h2>188 </div>189 <dl className="fd-hwn-stats">190 {stats.map(([v, l]) => <div key={l}><dt>{l}</dt><dd>{v}</dd></div>)}191 </dl>192 </div>193 <div className="fd-hwn-desk" ref={desk}>194 <div className="fd-hwn-ruler" aria-hidden="true" />195 <p className="fd-hwn-tip" aria-hidden="true">196 <svg width="34" height="22" viewBox="0 0 34 22" aria-hidden="true"><path d="M2 4c8 0 14 2 20 8 2.4 2.4 4 5 5 8M27 20l-1-6M27 20l-6-2" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /></svg>197 drag them around198 </p>199 {notes.map((n) => {200 const z = order.indexOf(n.id) + 1;201 const long = n.text.length > 70;202 return (203 <figure204 key={n.id}205 ref={(el) => { if (el) els.current.set(n.id, el); else els.current.delete(n.id); }}206 className="fd-hwn-note"207 data-kind={n.kind}208 data-hand={!long || undefined}209 data-top={z === order.length || undefined}210 tabIndex={0}211 aria-roledescription="movable note"212 aria-label={`Note from ${n.name}, ${n.meta}: ${n.text}`}213 style={{ left: `${n.x}%`, top: n.y, width: n.w, zIndex: z, transform: `rotate(${n.rot}deg)`, ["--rot" as string]: `${n.rot}deg` }}214 onPointerDown={onDown(n.id)}215 onPointerMove={onMove(n.id)}216 onPointerUp={onUp(n.id)}217 onPointerCancel={onUp(n.id)}218 onFocus={() => front(n.id)}219 onKeyDown={onKey(n.id)}220 >221 {n.kind === "postcard" && <span className="fd-hwn-stamp" aria-hidden="true"><span>30</span><i>DAYS</i></span>}222 {n.photo && (223 <span className="fd-hwn-photo" aria-hidden="true">224 <img src={n.photo} alt="" width={84} height={96} loading="lazy" decoding="async" draggable={false} />225 <i className="fd-hwn-tape" />226 </span>227 )}228 {(n.kind === "index" || n.kind === "kraft" || n.kind === "graph") && !n.photo && <i className="fd-hwn-tape fd-hwn-tape-top" aria-hidden="true" />}229 <blockquote className="fd-hwn-text" aria-hidden="true"><p>{n.text}</p></blockquote>230 <figcaption className="fd-hwn-sig" aria-hidden="true">231 <span className="fd-hwn-name">{n.name}</span>232 <span className="fd-hwn-meta">{n.meta}</span>233 </figcaption>234 </figure>235 );236 })}237 <button type="button" className="fd-hwn-reset" onClick={putBack} data-show={moved || undefined} aria-hidden={!moved || undefined} tabIndex={moved ? 0 : -1}>238 <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2.5 7a4.5 4.5 0 107.7-3.2M10.5 1.5v2.5H8" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>239 Put them back240 </button>241 </div>242 </section>243 );244}245 246const SERIF = `"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif`;247const SANS = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;248const MONO = `ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace`;249const HAND = `"Bradley Hand","Segoe Print","Marker Felt","Chalkboard SE","Comic Neue",cursive`;250 251const css = `252.fd-hwn,.fd-hwn *,.fd-hwn *::before,.fd-hwn *::after{box-sizing:border-box}253.fd-hwn{--fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;254 --fd-hwn-mat:#1E3B30;--fd-hwn-grid:rgba(214,232,214,.09);--fd-hwn-grid2:rgba(214,232,214,.18);--fd-hwn-cream:#EFE8D6;--fd-hwn-mut:#9DB5A6;--fd-hwn-ink:#2A2620;--fd-hwn-marker:#F2C14E;255 display:block;max-width:1240px;margin:0 auto;padding:72px 32px 80px;background:#15291F;color:var(--fd-hwn-cream);256 font:400 15px/1.5 ${SANS};letter-spacing:0;-webkit-font-smoothing:antialiased}257.fd-hwn-head{display:flex;align-items:flex-end;justify-content:space-between;gap:40px;margin:0 0 32px;padding:0}258.fd-hwn-eyebrow{margin:0 0 14px;padding:0;font:500 12px/1.2 ${MONO};letter-spacing:.1em;text-transform:uppercase;color:var(--fd-hwn-marker)}259.fd-hwn-title{margin:0;padding:0;max-width:18ch;font:400 clamp(2.25rem,4.6vw,3.75rem)/1.02 ${SERIF};letter-spacing:-.03em;color:var(--fd-hwn-cream);text-wrap:balance}260.fd-hwn-stats{display:flex;gap:32px;margin:0;padding:0 0 6px}261.fd-hwn-stats div{display:flex;flex-direction:column-reverse;gap:6px;margin:0;padding:0 0 0 16px;border-left:1px solid rgba(239,232,214,.2)}262.fd-hwn-stats dt{margin:0;padding:0;font:400 12px/1.3 ${SANS};color:var(--fd-hwn-mut)}263.fd-hwn-stats dd{margin:0;padding:0;font:400 1.75rem/1 ${SERIF};letter-spacing:-.02em;color:var(--fd-hwn-cream)}264.fd-hwn-desk{position:relative;height:720px;margin:0;padding:0;border-radius:18px;overflow:hidden;touch-action:pan-y;265 background-color:var(--fd-hwn-mat);266 background-image:linear-gradient(var(--fd-hwn-grid2) 1px,transparent 1px),linear-gradient(90deg,var(--fd-hwn-grid2) 1px,transparent 1px),linear-gradient(var(--fd-hwn-grid) 1px,transparent 1px),linear-gradient(90deg,var(--fd-hwn-grid) 1px,transparent 1px);267 background-size:120px 120px,120px 120px,24px 24px,24px 24px;background-position:-1px -1px;box-shadow:inset 0 0 0 1px rgba(214,232,214,.12),inset 0 0 120px rgba(0,0,0,.25)}268.fd-hwn-ruler{position:absolute;left:0;right:0;bottom:0;height:22px;pointer-events:none;269 background-image:linear-gradient(90deg,rgba(214,232,214,.55) 1px,transparent 1px),linear-gradient(90deg,rgba(214,232,214,.3) 1px,transparent 1px);background-size:120px 22px,24px 10px;background-repeat:repeat-x;background-position:-1px 0,-1px 12px}270.fd-hwn-tip{position:absolute;right:28px;bottom:36px;display:flex;align-items:flex-end;gap:6px;margin:0;padding:0;transform:rotate(-4deg);font:400 1.125rem/1 ${HAND};color:var(--fd-hwn-marker);opacity:.9;pointer-events:none}271.fd-hwn-tip svg{display:block;transform:scaleX(-1)}272.fd-hwn-note{position:absolute;display:block;margin:0;padding:22px 22px 18px;border-radius:2px;color:var(--fd-hwn-ink);background:#FBFAF6;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none;outline:none;will-change:transform;273 box-shadow:0 1px 1px rgba(0,0,0,.18),0 10px 24px -8px rgba(0,0,0,.45);transition:box-shadow var(--fd-dur-2) var(--fd-ease-out)}274.fd-hwn-note:active{cursor:grabbing;box-shadow:0 2px 2px rgba(0,0,0,.15),0 28px 50px -12px rgba(0,0,0,.55)}275.fd-hwn-note:focus-visible{box-shadow:0 0 0 2px var(--fd-hwn-mat),0 0 0 4px rgba(242,193,78,.75),0 10px 24px -8px rgba(0,0,0,.45)}276.fd-hwn-note[data-kind="index"]{background-color:#FBFAF6;background-image:linear-gradient(transparent 39px,rgba(209,74,64,.55) 39px,rgba(209,74,64,.55) 40px,transparent 40px),repeating-linear-gradient(transparent 0 23px,rgba(80,120,170,.28) 23px 24px);background-position:0 0,0 40px;padding-top:12px}277.fd-hwn-note[data-kind="index"] .fd-hwn-text{margin-top:36px}278.fd-hwn-note[data-kind="sticky"]{background:#F6DF74;border-radius:1px 1px 18px 1px/1px 1px 6px 1px;padding:24px 22px 20px}279.fd-hwn-note[data-kind="pink"]{background:#F3B7A9;border-radius:1px 1px 1px 16px/1px 1px 1px 6px;padding:20px 20px 18px}280.fd-hwn-note[data-kind="kraft"]{background:#C9A57A;color:#2B2116;background-image:radial-gradient(rgba(90,60,30,.12) 1px,transparent 1.5px);background-size:7px 7px}281.fd-hwn-note[data-kind="graph"]{background-color:#F7F7F1;background-image:linear-gradient(rgba(60,140,140,.2) 1px,transparent 1px),linear-gradient(90deg,rgba(60,140,140,.2) 1px,transparent 1px);background-size:14px 14px;282 -webkit-mask:radial-gradient(5px at 50% 0,transparent 98%,#000) 0 0/14px 100% repeat-x;mask:radial-gradient(5px at 50% 0,transparent 98%,#000) 0 0/14px 100% repeat-x;padding-top:26px}283.fd-hwn-note[data-kind="postcard"]{background:#F2EBDC;padding:22px 26px 20px;background-image:linear-gradient(90deg,transparent calc(62% - 1px),rgba(42,38,32,.22) calc(62% - 1px),rgba(42,38,32,.22) 62%,transparent 62%)}284.fd-hwn-note[data-kind="postcard"] .fd-hwn-text{width:58%}285.fd-hwn-note[data-kind="postcard"] .fd-hwn-sig{width:58%}286.fd-hwn-note[data-kind="postcard"] .fd-hwn-photo{position:absolute;right:22px;top:auto;bottom:20px;transform:rotate(3deg)}287.fd-hwn-stamp{position:absolute;right:20px;top:18px;display:flex;flex-direction:column;align-items:center;justify-content:center;width:52px;height:60px;border:2px dashed rgba(192,72,52,.75);color:#B6452F;transform:rotate(4deg)}288.fd-hwn-stamp span{display:block;font:700 1.25rem/1 ${SERIF}}289.fd-hwn-stamp i{display:block;font:600 8px/1 ${MONO};font-style:normal;letter-spacing:.14em}290.fd-hwn-photo{position:relative;float:right;display:block;width:76px;margin:-4px -4px 8px 12px;padding:5px 5px 14px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);transform:rotate(4deg)}291.fd-hwn-photo img{display:block;width:66px;height:72px;object-fit:cover;object-position:50% 25%;pointer-events:none}292.fd-hwn-tape{position:absolute;left:50%;top:-10px;display:block;width:56px;height:18px;margin-left:-28px;background:rgba(240,236,214,.62);box-shadow:0 1px 1px rgba(0,0,0,.08);transform:rotate(-6deg)}293.fd-hwn-photo .fd-hwn-tape{top:-9px;width:44px;margin-left:-22px;transform:rotate(8deg)}294.fd-hwn-text{margin:0;padding:0}295.fd-hwn-text p{margin:0;padding:0;font:400 1.0625rem/1.42 ${SERIF};letter-spacing:-.005em;color:inherit;text-wrap:pretty}296.fd-hwn-note[data-kind="index"] .fd-hwn-text p{line-height:24px}297.fd-hwn-note[data-hand] .fd-hwn-text p{font:400 1.375rem/1.25 ${HAND};letter-spacing:0}298.fd-hwn-sig{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 10px;clear:both;margin:14px 0 0;padding:0}299.fd-hwn-name{font:400 1.25rem/1 ${HAND};color:#1F3F8F}300.fd-hwn-note[data-kind="kraft"] .fd-hwn-name,.fd-hwn-note[data-kind="sticky"] .fd-hwn-name,.fd-hwn-note[data-kind="pink"] .fd-hwn-name{color:#2B2116}301.fd-hwn-meta{font:500 10px/1.2 ${MONO};letter-spacing:.08em;text-transform:uppercase;color:rgba(42,38,32,.6)}302.fd-hwn-reset{appearance:none;-webkit-appearance:none;position:absolute;left:24px;bottom:36px;z-index:100;display:inline-flex;align-items:center;gap:8px;height:36px;margin:0;padding:0 14px;border:0;border-radius:9999px;background:var(--fd-hwn-cream);color:#15291F;cursor:pointer;303 font:600 13px/1 ${SANS};opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity var(--fd-dur-2),transform var(--fd-dur-3) var(--fd-spring)}304.fd-hwn-reset svg{display:block}305.fd-hwn-reset[data-show]{opacity:1;transform:none;pointer-events:auto}306.fd-hwn-reset:active{transform:scale(.96)}307.fd-hwn-reset:focus-visible{outline:2px solid rgba(242,193,78,.75);outline-offset:2px}308@media (max-width:1100px) and (min-width:761px){.fd-hwn-desk{height:900px}}309@media (max-width:760px){310 .fd-hwn{padding:48px 16px 56px}311 .fd-hwn-head{flex-direction:column;align-items:flex-start;gap:24px;margin-bottom:24px}312 .fd-hwn-stats{gap:16px;flex-wrap:wrap}313 .fd-hwn-stats dd{font-size:1.375rem}314 .fd-hwn-desk{height:auto;display:flex;flex-direction:column;align-items:center;padding:40px 12px 96px;overflow:hidden}315 .fd-hwn-note{position:relative;left:auto!important;top:auto!important;max-width:88%;margin:0 0 -6px;touch-action:pan-y}316 .fd-hwn-note:nth-of-type(odd){align-self:flex-start}317 .fd-hwn-note:nth-of-type(even){align-self:flex-end}318 .fd-hwn-note[data-kind="postcard"]{max-width:100%}319 .fd-hwn-tip{display:none}320 .fd-hwn-reset{bottom:36px}321}322@media (prefers-reduced-motion:reduce){.fd-hwn *{transition-duration:1ms!important}}323@media (prefers-contrast:more){.fd-hwn-note{box-shadow:0 0 0 1px #000}}324`;325 326export function Demo() {327 return (328 <div className="fd-hwnd">329 <style>{demoCss}</style>330 <header className="fd-hwnd-bar">331 <span className="fd-hwnd-logo">Line <i>&</i> Wash</span>332 <span className="fd-hwnd-next">Next cohort starts 1 December</span>333 <a className="fd-hwnd-cta" href="#enrol">Save a seat, €89</a>334 </header>335 <HandwrittenNotes />336 </div>337 );338}339 340const demoCss = `341.fd-hwnd,.fd-hwnd *{box-sizing:border-box}342.fd-hwnd{min-height:100vh;margin:0;padding:0;background:#15291F;font:400 14px/1.4 ${SANS}}343.fd-hwnd-bar{display:flex;align-items:center;gap:24px;max-width:1240px;height:64px;margin:0 auto;padding:0 32px;border-bottom:1px solid rgba(239,232,214,.1)}344.fd-hwnd-logo{font:400 1.375rem/1 ${SERIF};letter-spacing:-.02em;color:#EFE8D6}345.fd-hwnd-logo i{font-style:italic;color:#F2C14E}346.fd-hwnd-next{margin-left:auto;font:400 13px/1 ${SANS};color:#9DB5A6}347.fd-hwnd-cta{display:inline-flex;align-items:center;height:36px;padding:0 16px;border-radius:9999px;background:#F2C14E;color:#15291F;font:600 13px/1 ${SANS};text-decoration:none}348.fd-hwnd-cta:hover{background:#EFE8D6}349.fd-hwnd-cta:focus-visible{outline:2px solid rgba(242,193,78,.75);outline-offset:2px}350@media (max-width:760px){.fd-hwnd-bar{padding:0 16px}.fd-hwnd-next{display:none}.fd-hwnd-cta{margin-left:auto}}351`;More Testimonials. Same file contract.
Questions about Handwritten Notes
Run npx shadcn@latest add https://freedesignmd.com/r/handwritten-notes.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.
Testimonial notes scattered on a cutting mat: drag cards with spring physics and click to bring one forward. It is a free, MIT-licensed React testimonials component from the freedesignmd library.
Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.
All evergreen browsers (Pointer Events). On narrow screens notes stack and drag sideways so the page still scrolls.
Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.


