Most skeleton loaders are a gray <div> sized to 1em. That gets the height in the right ballpark and throws away everything else that makes a block of text read like text: real paragraphs wrap into multiple lines with a shorter last line, letters aren’t all the same height — some poke up (ascenders), some hang below the baseline (descenders) — and none of that varies with font size the way real type does.
So <text-skeleton>: a web component that reads the font actually in effect at its position in the DOM — via a canvas measureText() call, the same trick you’d use to lay out real text — and generates blobs that respect that font’s actual ascent, x-height, and descender depth. A 28px bold heading gets chunkier, taller blobs with more line spacing than a 13px caption, automatically, because it’s reading the same numbers a real heading would use.

The fun part was the edge cases. Getting individual character blobs to look right was the easy 80%. The last 20% turned into a small pile of very specific, very satisfying bugs:
- Two semi-transparent blobs overlapping compose darker than either alone — the classic alpha double-coverage trap, which meant the “connected” word-shapes (one continuous bar instead of dashed blobs) needed geometry that never actually overlaps, not just careful z-ordering.
- A shimmer that gets brighter toward its peak runs out of headroom the moment you crank intensity to 100% — there’s nowhere “more opaque than opaque” to go. Flipping it to dip toward transparent instead fixed that permanently, for free, at every intensity.
- Custom element constructors aren’t allowed to add attributes to themselves —
this.style.display = 'contents'in a constructor throws, because setting a CSS property reflects into thestyle=""attribute. Learned that one the hard way, twice, in two different components. - A flex item with no explicit
flex-growgets its width from its content’s intrinsic size — and every blob here isposition: absolute, which contributes zero intrinsic width, unlike real text. A perfectly innocent-looking flex row collapsed to nothing the moment real text was swapped for skeleton content, for a reason that had nothing to do with the skeleton itself.
Once the block-of-text version worked, the same machinery turned out to solve a completely different problem. If you already know the real text — because it’s sitting in the DOM, not because it hasn’t loaded yet — you can run the same font-aware layout in reverse: hide real content instead of standing in for missing content. That’s <text-censor>: wrap real markup, and it swaps every text node for a matching skeleton on demand. Same visual language, opposite job — built for redacted screenshots and screen recordings rather than loading states.

Where I’d take it next: a browser extension that lets you click-to-redact arbitrary elements on any page before a screenshot, using the same leaf-detection logic. That’s a genuinely different architecture (a content script reacting to whatever you click, not a component you wrap around markup you own) — but it’s the same core idea wearing a different hat.
Both components: zero dependencies, no build step, MIT

