I wanted my landing page to feel a bit more personal, so I added a hand-drawn signature that animates itself — like watching someone sign their name in real time. Here’s the full process from creating the SVG to the final implementation, including the LLM prompt I used to build it.
Step 1: Creating the Signature SVG
Drawing the Signature
I used DocHub — a free document signing tool — to draw my signature. Any tool that lets you sign with a mouse/trackpad/stylus and export as SVG works. Some alternatives:
- DocHub (what I used) — sign a PDF, then export/download the signature as SVG
- SVG signature pad libraries — like
signature_padby Szymon Nowak, which can export directly to SVG - iPad/tablet apps — draw in any app, export as SVG
- Figma/Illustrator — draw with the pen tool, export as SVG
Why the Export Format Matters
The key requirement is that the SVG must use stroked paths (fill="none", stroke="black"), not filled shapes. DocHub’s export is perfect for this because it outputs the signature as a series of small cubic bezier <path> segments with round line caps:
<path d="M 54.430,55.914 C 51.210,54.968 51.238,54.889 48.047,53.863"
stroke-width="4.818" stroke="black" fill="none" stroke-linecap="round" />My signature SVG had ~120 of these small path segments plus a <circle> element for the dot on the “i”. Each segment is a short curve — the tool captures your pen movement as many small strokes rather than one continuous path. This actually works in our favor for the animation, because we can animate each segment sequentially.
If your SVG has filled shapes instead of strokes (common with some export tools), you’d need to convert them to stroked paths first, which is more complex. Stick with a tool that gives you stroke-based output.
Step 2: The LLM Prompt
I used Claude to implement the entire thing. Here’s roughly the prompt I gave it, which you can adapt for your own site:
Add an animated SVG signature that draws itself on the landing page. The signature SVG contains stroked paths (
fill="none",stroke="black",stroke-linecap="round") with ~120 small path segments and a circle element. The draw-on animation should use CSSstroke-dasharray/stroke-dashoffsetwith sequential delays calculated proportionally from each segment’s length.Requirements:
- All paths get class
signature-path, the circle gets classsignature-dot- Container div with class
signature-container- Replace hardcoded
stroke="black"withstroke="currentColor"for theme support- Use the exact SVG content from my exported file
- The inline script should: calculate each path’s
getTotalLength(), set dasharray/dashoffset, compute cumulative delays proportional to segment length (total animation ~2.5s), set CSS custom properties (—delay, —duration, —easing), then add.animateclass to trigger- Paths start at
opacity: 0until JS sets them up- Only show on the landing page (conditional render on index slug)
- SPA compatible (listen to
navevent, notDOMContentLoaded)
The key details in this prompt:
- Specifying the technique (
stroke-dasharray/stroke-dashoffset) — this avoids the LLM guessing at approaches - Proportional timing — without this, you’d get uniform timing which looks robotic
currentColor— dark mode support for freeopacity: 0initial state — prevents flash of unstyled content before JS runs- SPA compatibility — critical for Quartz which uses client-side navigation
If you’re using a different static site generator (Astro, Next.js, Hugo, etc.), adjust the last two points to match your framework’s routing/lifecycle.
Step 3: How the Animation Works
The Core Trick: stroke-dasharray
This is a well-known SVG/CSS trick, but it’s worth understanding:
stroke-dasharraydefines a dash pattern. Set it to the path’s total length → one dash that covers the entire pathstroke-dashoffsetshifts where the dash starts. Set it to the same length → the dash is shifted completely off, path invisible- Animate
stroke-dashoffsetto0→ the dash slides into view, path “draws” itself
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}The browser method path.getTotalLength() returns the exact pixel length of any SVG path. This is essential because each segment has a different length, and the dasharray value must match exactly.
Sequencing ~120 Segments
With so many path segments, each one needs to animate in order with no gaps. The approach:
- Loop through all paths, call
getTotalLength()on each, store the lengths - Sum up the total length across all segments
- For each segment, calculate its proportional duration:
(segmentLength / totalLength) * TOTAL_DURATION - Track a cumulative delay — each segment starts right when the previous one ends
const TOTAL_DURATION = 2.5 // seconds
const EASE = "cubic-bezier(0.4, 0, 0.2, 1)"
let totalLength = 0
const lengths: number[] = []
paths.forEach((path) => {
const len = path.getTotalLength()
lengths.push(len)
totalLength += len
})
let cumulativeDelay = 0
paths.forEach((path, i) => {
const len = lengths[i]
const duration = (len / totalLength) * TOTAL_DURATION
path.style.strokeDasharray = `${len}`
path.style.strokeDashoffset = `${len}`
path.style.setProperty("--delay", `${cumulativeDelay}s`)
path.style.setProperty("--duration", `${duration}s`)
path.style.setProperty("--easing", EASE)
cumulativeDelay += duration
})This proportional timing is what makes it look natural — longer strokes take more time, short flicks are quick. Without it (i.e., uniform duration per segment), the animation would look mechanical and wrong.
The CSS
Paths start invisible and only animate once JS has set everything up and added the .animate class:
.signature-path, .signature-dot {
opacity: 0;
}
.signature-container.animate .signature-path {
animation:
draw var(--duration) var(--easing) var(--delay) forwards,
fade-in 0.01s var(--delay) forwards;
}
.signature-container.animate .signature-dot {
animation: fade-in 0.15s var(--delay) forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
@keyframes fade-in {
to { opacity: 1; }
}The fade-in animation is a near-instant (0.01s) opacity flip that happens at the same --delay as the draw. Without it, you’d see a brief flash of all paths at once before the draw animation starts, because CSS stroke-dashoffset only hides the stroke visually — the element itself would still be rendered at full opacity.
The dot on the “i” gets a gentler 0.15s fade-in at roughly the midpoint of the animation (since the dot appears in the middle of the signature).
Step 4: Theme Support
The original SVG had stroke="black" on every path and fill="black" on the circle. Replacing both with currentColor makes the signature automatically follow whatever text color your CSS theme defines:
<!-- Before -->
<path stroke="black" fill="none" ... />
<circle fill="black" ... />
<!-- After -->
<path stroke="currentColor" fill="none" ... />
<circle fill="currentColor" ... />Zero JS logic needed for dark mode. The signature just inherits the text color.
Step 5: Quartz-Specific Integration
If you’re using Quartz, here’s the component structure:
File structure
quartz/components/
Signature.tsx # Component with inline SVG
styles/signature.scss # Animation styles
scripts/signature.inline.ts # Runtime animation setup
index.ts # Add export here
Component pattern
Quartz components use Preact (class not className), and attach styles/scripts as static properties:
const Signature: QuartzComponent = (_props: QuartzComponentProps) => {
return (
<div class="signature-container">
<svg ...>
{/* paste all your paths here with class="signature-path" */}
{/* circle with class="signature-dot" */}
</svg>
</div>
)
}
Signature.css = style
Signature.afterDOMLoaded = scriptThe SVG is rendered inline (not as an <img> or <object>) so the browser has direct DOM access to each <path> element. This is required for getTotalLength() to work and for the CSS custom properties to be set on each element.
Conditional rendering (index page only)
In quartz.layout.ts:
afterBody: [
Component.ConditionalRender({
component: Component.Signature(),
condition: (page) => page.fileData.slug === "index",
}),
],SPA navigation
Quartz uses client-side routing, so DOMContentLoaded only fires once. The script listens to Quartz’s nav event instead, which fires on every page transition:
document.addEventListener("nav", () => setupSignature())This means the animation replays every time you navigate back to the landing page — which is the behavior you want.
Adapting for Other Frameworks
The core technique (dasharray + proportional delays) works anywhere. Here’s what changes per framework:
- Plain HTML — put the script in a
<script>tag, useDOMContentLoaded - React/Next.js — use a
useEffecthook, refs for the container,currentColorworks the same - Astro — use a
<script>tag in an.astrocomponent, or a framework component - Hugo — inline the SVG in a partial, add the script to a
<script>tag
The SVG must be inlined in the HTML (not loaded as an image) for the animation to work.
Result
The signature draws itself over ~2.5s when you land on the homepage. It feels organic because the timing is proportional to each stroke’s length — fast flicks are fast, long curves are slow. The dot on the “i” fades in at roughly the right moment. Dark mode just works.
Small detail, but it makes the page feel less like a static site and more like something someone actually made.