<text-skeleton>

A loading placeholder that reads the font actually in effect (size, line-height, x-height, ascent, descender depth) and lays out gray blobs the way a real paragraph would wrap — including a shorter last line.

Side by side — real text vs. skeleton (same font, same width)

real
Good documentation explains the why behind a decision, not just what the code does — the what is already visible in well-named identifiers and the diff itself.
<text-skeleton lines="3">

Swap in place — proves there's no layout shift

Click to swap between the real paragraph and its skeleton at the exact same width/font. The box height should not jump.

Web components let you ship a custom element without a framework: register a tag, attach a shadow root, and the browser handles the rest — including style encapsulation and lifecycle callbacks like connectedCallback and attributeChangedCallback.

Apples-to-apples — text attribute

Feed the skeleton the same real string via the text attribute: word lengths, per-character ascender/x/descender shape, and glyph widths all come from the real letters, so it wraps to the same line count as the real paragraph below, at the same width.

line(s), real text
Type-driven layout is one of those things that looks trivial until you try to fake it convincingly with placeholder blobs instead of glyphs.
line(s), text attribute

checking…

Variance — how "busy" invented text looks

Without a text attribute, blob shapes are invented from the variance knob (0–1) instead of real letters. Skimmed real prose reads mostly flat to the eye, so the default (0.2) stays close to that; 1 matches the actual letter-shape frequency of English — busier than most people expect.

variance="0" (mostly x-height, still ~20% tall/hanging)
default (variance="0.2")
variance="1" (realistic English mix)

Font-metric adaptation

Same component, no props changed except the surrounding font — ascent, x-height and descender depth are re-measured from the computed style each time.

sans-serif, 16px
serif, 16px
monospace, 16px
bold, 28px

Headings vs. body text

Inline skeleton mid-sentence

Real text flowing right into a skeleton and back to real text again, all on one baseline.

Small print / labels

· ·

Reflow on resize

Drag the bottom-right corner of the box below — line breaks and the short last line recompute for the new width, same as real text.

Animation modes

animated="pulse" (default) shimmers every blob in sync. animated="sweep" runs one light band across the whole block via a single shared requestAnimationFrame loop (not per-instance). animated="typing" reveals blobs left-to-right/line-by-line, holds, erases in the same order, and repeats — driven entirely by staggered animation-delay, no JS timer.

animated="pulse" (default)
animated="sweep"
animated="typing"
animated="false"

Connected characters

Separate blobs read as a dashed line. connect="word" fills the gaps within each word with an x-height bar (word rhythm stays visible). connect="line" fills the gaps across the whole line too, including the inter-word spaces — no word boundaries left, closer to a redaction bar. Ascenders/descenders still poke out either way.

connect="none" (default)
connect="word"
connect="line"
connect="word" + animated="sweep"

Custom colors via CSS variables

Putting it together — a card

A realistic title + subtitle + 2-line body + footer actions card. Real content on the left, an all-<text-skeleton> placeholder on the right — same markup, same CSS, just the text nodes swapped for the component. The controls below restyle every skeleton in the card live.

real

Senior Frontend Engineer

Zurich, Switzerland · Full-time · Remote-friendly

We're looking for an experienced engineer to help us build the next generation of our platform.

<text-skeleton>

Censoring real content — <text-censor>

A different job from everything above: that's all a placeholder for content you don't have yet. This wraps markup that's already there and swaps its text for matching <text-skeleton text="…"> placeholders on demand — redacted screenshots, screen recordings, "blur sensitive fields" UI. It never invents anything; every placeholder is driven by the real text it's standing in for, so toggling never shifts layout. The icon in "Apply now" is marked data-censor-skip and stays put either way. This card is intentionally small — see censor-demo.html for a full page (nav, table, accordion, form) stress-testing the same wrapper.

Senior Frontend Engineer

Zurich, Switzerland · Full-time · Remote-friendly

We're looking for an experienced engineer to help us build the next generation of our platform.