Signatures that sign themselves
Aug 10, 2026
The mark at the top of this page wrote itself when you loaded it. It isn't a video, or a GIF, or a Lottie file with a player behind it. It's an SVG — 1.3 kilobytes — and there is no JavaScript inside it.
The same signature exported as an animated GIF is 228 kilobytes. That is 175 times bigger, and it looks worse.
That gap isn't compression. It's representation. A GIF of a signature is ninety photographs of a hand moving. The SVG is the movement.
I built a tool called Penna to make them, mostly because I wanted this one.
- To
- hiring@example.com
- Subject
- Re: the frontend role
Thanks for the notes — revised deck attached.
Best,
Haris Vreto · Senior Full-stack Engineer
harisvreto.dev
A signature is a movement, not a picture
When you sign your name you don't produce an image. You produce a gesture that happens to leave ink behind. The image is residue.
So when a drawing pad records your signature, it already has the interesting thing: an ordered list of points, in the order your hand made them. Rasterising that to pixels throws away the order, and then an animation format has to spend hundreds of kilobytes storing it again, badly, as frames.
Keep the order and the file gets absurdly small, because you are no longer shipping the drawing. You're shipping the instructions and letting the browser draw.
The whole trick is two attributes
Every stroke becomes one path. Give it a dash pattern exactly as long as the path itself, then offset the dash by that same amount — the line is now technically drawn, but the dash sits entirely off the end of it, so nothing is visible. Walk the offset back to zero and the stroke appears from its start to its finish. That's the pen.
@keyframes a2 {
0%, 23.5% { stroke-dashoffset: 323.8 }
33.2%, 100% { stroke-dashoffset: 0 }
}
.k2 { animation: a2 2.6s linear infinite }That's a real rule, copied out of a real export — which is why the numbers are ugly. Nobody picked 23.5% and 33.2%. They are that stroke's start and end divided by the total cycle, rounded to one decimal, and they come out wherever the geometry puts them.
The two flat sections are what makes a multi-stroke signature work: every stroke shares one animation duration, so stroke three holds itself invisible until its turn, draws, then holds finished until the loop restarts. One duration, one inline style block, no coordination code. The stagger falls out of the percentages.
You could round them to 20% and 35% and the animation would still play. It would also be wrong — you'd have overridden the arc length with a number you liked the look of, and the pen would change speed between strokes for no physical reason. The awkward decimals are the evidence that nothing was hand-placed.
The browser has been able to do this the whole time. It needs no runtime because CSS is the runtime.
Timing has to come from geometry
The first version gave every stroke the same duration. It looked wrong immediately, and it took me a minute to work out why: a long looping flourish and a short tick were finishing in the same beat, so the pen appeared to sprint through the big strokes and crawl through the small ones.
A hand doesn't do that. A hand moves at roughly one speed and takes longer on longer strokes. So duration has to be derived from arc length:
const dur = Math.max(0.16, length / (620 * speed));620 pixels per second, with a floor so a dot still registers, and a 0.14s pause between strokes for the pen lifting. Three numbers. That single change is the whole difference between something that reads as handwriting and something that reads as playback.
It's a good reminder that the physical plausibility usually lives in one derived value, not in more keyframes.
The part I got wrong
I shipped SVG and GIF and thought I was done. Then I put the SVG in a GitHub README and it sat there frozen — GitHub proxies images and strips the style block, so the animation never runs. The one place I most wanted to show it off was the one place it couldn't work.
The obvious fallback was the GIF, except GIF transparency is one bit: a pixel is either fully opaque or fully gone. A signature is antialiased round-cap strokes, which are almost entirely partial alpha along every edge. A transparent GIF doesn't give you a clean cutout, it gives you a jagged halo, which is why the GIF export mattes onto a solid colour and looks stuck to a rectangle.
So there's a third export now, APNG, which carries real 8-bit alpha at 14.8 KB. It exists entirely because of a rendering quirk on one website. Most format decisions are like this — not chosen on merit, chosen because of where the file has to survive.
What it costs
Limits, so this doesn't read like a brochure.
- No pressure, no variable width. Every stroke is one flat thickness. Real ink thins on fast strokes and pools on slow ones, and none of that survives.
- You can't scrub it. CSS animations have no scrubber without JavaScript, and adding JavaScript would forfeit the entire point.
- Percentages are quantised. Timings round to one decimal, so a very long signature has slightly lumpy stroke boundaries. Nobody has ever noticed. I know it's there.
- It's an image that executes. A self-animating SVG runs CSS, which is why sites like GitHub sanitise it. Being unusually capable for an image file is exactly what makes it untrusted.
The honest summary is that this is a small trick that has been available since roughly 2014, and I mostly assembled known parts — Claude Code drafted the export path, and I spent my time on the timing model and on finding out the hard way what GitHub does to SVGs.
But the file is 1.3 KB, it animates in an img tag, it works offline, and it will still work in ten years, because nothing in it can go out of date. The stack is: the browser.
A GIF stores what a signature looked like. An SVG stores what your hand did.
Questions about this one? The terminal twin has opinions — ⌘K