Automatic Skeletonization

How the DOM-scan engine classifies elements and preserves layout.

Automatic Skeletonization

The heart of the module is a DOM-scanning engine that runs inside every active <Skeletonizer>. It walks the rendered subtree (leaf-stop DFS), measures every element it will skeletonize, classifies it, and renders the whole result as one <svg> overlay positioned above the (hidden but still laid-out) real content — without ever moving, wrapping or removing a node.

What it recognises

The engine classifies elements into semantic bone kinds:

text · heading · image · avatar · badge · tag · button · icon · input · textarea · select · checkbox · radio · switch · plus generic block / container and custom components.

Classification uses tag names, ARIA roles, computed styles and structure (e.g. a roughly-square element with a circular radius becomes an avatar; an <img> becomes an image).

Layout fidelity — a structural replica, not an approximation

The skeleton is not a rough stand-in: it is measured to be a structurally faithful replica of the real layout, coordinate for coordinate.

Real element  →  Bounding box (scan)  →  SVG blueprint  →  Rendered skeleton
  312×48px         { x, y, w:312,          <rect width="312"       identical
  rx 8px            h:48, radius:8 }          height="48" rx="8"/>   312×48 box

What is preserved exactly, with no invented values:

  • Width & height — read via getBoundingClientRect() and carried through to the SVG width/ height unchanged (rounded to svgPrecision decimal places, so the blueprint matches the real box to within ±1px). No arbitrary widths, no percentage guesses, no random shrinkage.
  • Border radius — the element's own computed border-radius maps straight onto the shape's rx/ry (or a <circle> when the element is square and visibly round — see Avatars below).
  • Text lines — a wrapped text node is measured line by line (Range.getClientRects()), so a paragraph that wraps into 3 real lines of 320px / 287px / 164px produces exactly 3 bones of 320px / 287px / 164px. The engine never falls back to an artificial 100% / 90% / 75% ladder when the real per-line width is available; only when no text-layout information exists at all (e.g. an empty leaf) does it fall back to the element's own box.
  • Images & avatars — an <img>/<picture>/<video> bone keeps the element's real width, height and aspect ratio (never deformed into a square). A roughly-square element with a visible circular radius becomes an avatar: a true <circle> when it reads as fully round, or a <rect rx> matching the measured radius when it is a rounded square instead.
  • Flex & grid structure — because every descendant is drawn at its own real absolute position (relative to the host), a grid never collapses into a vertical list and a flex row never collapses into a stack: the gaps and alignment are implicit in the measured coordinates. The scan layer additionally records each flex/grid container's display, flex-direction/ justify-content/align-items, and row/column gap for verification and DevTools/Explain — this metadata is a record of what the renderer already reproduces geometrically, not a separate approximation.

The renderer is deterministic: the same scanned nodes always produce the same SVG markup — no random line lengths, no cosmetic jitter.

How a bone is painted

  • The scan layer never mutates the real subtree — it only reads getBoundingClientRect() and getComputedStyle(). The real content is hidden with visibility: hidden (its layout box stays in place, which is exactly what makes the measurement possible).
  • The SVG renderer turns the measured nodes into one absolutely-positioned <svg> overlay: each bone is a <rect> (or <circle> for avatars/radios), filled by a shared, namespaced <linearGradient> that drives the shimmer sweep for every shape at once.
  • Disabling the skeleton removes the overlay and restores visibility: visible — the real markup was never touched.

Re-scanning

A debounced MutationObserver watches the subtree, so content added after the first scan (lazy lists, async components) is skeletonized automatically. You can also call refresh() manually.

Mixing with manual skeletons

Manual Skeleton* components are recognised and left untouched by the engine, so you can freely mix automatic and hand-authored skeletons in the same tree.

Steering it

Use the directivesv-skeleton-ignore, v-skeleton-keep, v-skeleton-replace, v-skeleton-union, v-skeleton-shimmer — to override the engine where you need precision.