DEV Community

Anas Abuelhaag
Anas Abuelhaag

Posted on

I built a handwriting engine in zero-dependency TypeScript (text, icons and charts that draw themselves)

Most "handwritten" text on the web is just a script font. It looks fine, but it's static,
and every "a" is identical to every other "a". Real handwriting isn't like that.

Check it here

So I built glyphed.js: a small library that renders text, icons, annotations and
charts as hand-drawn SVG that draws itself when it scrolls into view.

One line to use it

npm install glyphed.js
Enter fullscreen mode Exit fullscreen mode
import { write } from "glyphed.js";
write("#hello", "Hello, world!");
Enter fullscreen mode Exit fullscreen mode

That's it. The letters draw in, stroke by stroke, when #hello enters the viewport.

It's not a font

Every character is a real monoline SVG path on a 24-unit grid. Most glyphs ship with
~3 hand-drawn variants, and two layers of randomness make it read as handwriting:

  1. Variant picking. Each render picks a variant at random, but never the same one used last time for that character, so "hello" never has two identical l's in a row.
  2. Per-letter jitter. Every letter gets a small random rotation (±7°), baseline drift and scale (±10%), pivoted around its own center so the word stays aligned.

The same word never renders the same way twice. That's the point.

How the draw-in works

Each letter is a <path> with pathLength="1", stroke-dasharray: 1 and
stroke-dashoffset: 1, so it's fully hidden behind its own dash gap. Setting the offset
to 0 with a staggered CSS transition draws it left to right, letter by letter.

One gotcha I hit: a zero-length dash still paints its round cap, so every undrawn stroke
showed up as a dot. The fix is to start strokes with visibility: hidden and flip them
visible at the exact moment their draw begins.

No animation library, no requestAnimationFrame loop: just CSS transitions and an
IntersectionObserver.

Same pen, more things

Once you have a pen, you can draw anything with it.

Icons: 68 of them, on the same grid with the same wobble, and tree-shakable:

import { drawIcon } from "glyphed.js";
import { rocket } from "glyphed.js/icons";
drawIcon("#logo", rocket, { size: 48, label: "Launch" });
Enter fullscreen mode Exit fullscreen mode

Annotations: underline, circle, box, highlight, strike, cross and bracket on any element:

annotate("#price", { type: "circle", color: "#c63d24" });
Enter fullscreen mode Exit fullscreen mode

Charts: bar, line, pie and donut with hatched fills and handwritten labels, plus
sparklines. The palette is colorblind-checked, and hatch direction alternates as a
second cue.

drawChart("#stats", {
  type: "bar",
  data: [{ label: "Mon", value: 12 }, { label: "Tue", value: 19 }],
  title: "Commits per weekday",
});
Enter fullscreen mode Exit fullscreen mode

The details

  • Zero dependencies, ESM, strict TypeScript
  • renderText() returns a plain HTML string, so it works for SSR
  • One CSS variable for color: --hw-color
  • Text stays copyable and accessible
  • { animate: false } gives static output

Where it fits

Portfolio heroes, landing pages, blog posts, slide decks, dashboards that shouldn't look
like dashboards: anywhere you want the "sketched on a napkin" feel without shipping images.

⭐ GitHub: https://github.com/a-elhaag/glyphed.js
📦 npm: https://www.npmjs.com/package/glyphed.js

I'd love feedback, especially on glyph shapes you think look off. What would you build with it?

Top comments (1)