webdevpuneet.com

Skeleton Shape Morph Reveal — Skeleton Animates Into Real Content CSS JS

Skeleton Shape Morph Reveal · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="sm-wrap">
  <button class="sm-reload" id="smReload" type="button">Reload card</button>
  <div class="sm-card" id="smCard">
    <div class="sm-block sm-avatar" id="smAvatar"></div>
    <div class="sm-block sm-title" id="smTitle"></div>
    <div class="sm-block sm-body" id="smBody"></div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Skeleton blocks morph width, height, and border-radius directly to the real content\u2019s measured size
Off-screen probe element measures true content dimensions before animating
Two-phase reveal: shape settles first, then content fades in inside the resized box
Forced reflow ensures the resize transition is never skipped by paint batching
CSS transitions (not JS-driven frame stepping) handle the actual geometric animation
Distinct from a same-size opacity crossfade — the skeleton genuinely changes shape
Replayable demo via a reload button
Reusable measureNaturalSize() and morphBlock() helper functions
Zero dependencies — vanilla DOM measurement and CSS transitions
Works for content of unknown or variable size (names, bios, arbitrary text)

About this UI Snippet

Skeleton Shape Morph Reveal — Skeleton Blocks That Resize Into the Real Content, Not Just Fade Into It

Screenshot of the Skeleton Shape Morph Reveal snippet rendered live

The most common way to end a skeleton loading state is an instant swap or a crossfade — the skeleton fades out, the real content fades in, both occupying roughly the same space. This snippet does something more literal: it measures the real content's actual rendered size first, then animates the skeleton block's own width, height, and border-radius directly to that measured size, so the placeholder visibly reshapes itself into the real content's exact footprint before the content itself fades in on top.

Measuring real content off-screen

measureNaturalSize() creates a throwaway probe element positioned off-screen (position: absolute; left: -9999px), sets its inner HTML and styling to match what the real content will actually look like, appends it, reads its getBoundingClientRect(), and immediately removes it. This gives an accurate target width and height for content whose size can't be known in advance — a name of arbitrary length, a bio paragraph that wraps differently depending on its exact word count.

Animating the skeleton's own geometry

morphBlock() then sets the visible skeleton block's style.width, style.height, and style.borderRadius to those measured values. Because the CSS has transition rules on exactly those three properties, the block doesn't jump — it visibly grows, shrinks, or reshapes from its generic placeholder size into the real content's precise dimensions over half a second, using a real geometric transform rather than an opacity trick layered over a fixed-size container.

Content fades in only after the shape settles

A setTimeout matching the CSS transition duration waits for the resize to finish before swapping in the real markup and adding .sm-loaded, which fades the actual text or avatar in via its own opacity transition. The two-phase sequence — first the shape morphs, then the content appears inside the now-correctly-sized box — is what separates this from a same-size crossfade: the skeleton and the content are never pretending to be the same size the whole time, they genuinely aren't, and the morph is the part that reconciles them.

A forced reflow for a real starting point

When runReveal() resets the demo, it explicitly reads avatarEl.offsetWidth to force a synchronous reflow before the next morph begins. Without this, the browser could batch the size reset and the new target size into the same paint, skipping the visible transition entirely — the classic FLIP-technique gotcha this snippet works around.

Distinct from a same-size crossfade

This is a different technique from the skeleton-to-content crossfade loader, which layers two same-sized elements and fades between them. Here the skeleton block genuinely changes shape — useful whenever your placeholder's generic size doesn't match the real content's actual footprint, like a name that's much shorter than the skeleton bar suggested, or a bio that wraps to a different height than a fixed skeleton block guessed.

Customizing it

Swap the CONTENT object and probe styles for your real data shape, or drive the initial skeleton sizes and target sizes from actual API response metadata instead of a demo delay. Reuse measureNaturalSize() and morphBlock() against any element whose real content size is unknown until it arrives.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA card renders with shimmering skeleton blocks for an avatar, title, and body.
  2. 2
    Watch the shapes morphAfter a short delay, each block resizes to the real content\u2019s exact measured dimensions.
  3. 3
    Watch the content fade inOnce each block settles at its new size, the real avatar, name, and bio fade in inside it.
  4. 4
    Click "Reload card"The demo resets to skeleton shapes and morphs again, so you can replay the effect.
  5. 5
    Edit the CONTENT objectChange the name, initials, and bio text to see the morph adapt to different measured sizes.
  6. 6
    Reuse measureNaturalSize/morphBlockApply the same two functions to your own skeleton elements once real data arrives from an API.

Real-world uses

Common Use Cases

Profile cards with variable-length content
Names and bios of unpredictable length settle into their true size before appearing.
Dynamic dashboard widgets
Widgets whose real size depends on fetched data can morph rather than mismatch a fixed skeleton.
Teaching the FLIP measurement technique
A clear, minimal example of measuring off-screen before animating a real geometric transition.
Search result and comment previews
Placeholder rows that resolve into real, differently-sized content without a jarring layout jump.
Alternative to a same-size crossfade
Pairs as a contrast case with the skeleton to content crossfade loader when sizes genuinely differ.
Related: Suspense Fallback Card
See the Suspense Fallback Card for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A crossfade layers a skeleton and the real content at the SAME fixed size and fades opacity between them — the box never changes shape. This snippet measures the real content\u2019s true rendered size first, then animates the skeleton block\u2019s own width, height, and border-radius to that exact size, so the box genuinely reshapes itself before the content fades in inside it.

measureNaturalSize() creates a hidden probe element positioned off-screen with position: absolute and a large negative left offset, gives it the same styling and content the real element will have, appends it to the DOM just long enough to call getBoundingClientRect(), then removes it — giving an accurate size without ever flashing it on screen.

Browsers can batch multiple style changes into a single paint if they happen in the same tick, which would let a width reset and a new target width collapse into one jump with no visible transition. Reading offsetWidth synchronously forces the browser to compute layout at that point, guaranteeing the reset size is genuinely painted before the new target size is applied.

The morph still runs correctly — measureNaturalSize() always reflects whatever HTML and styles you pass it, so changing CONTENT.name or CONTENT.bio to a longer or shorter string automatically produces a different measured target size and a different morph animation, with no other code changes required.

Yes, as long as the probe element\u2019s styling matches the real element\u2019s actual constraints (e.g. a fixed width for text that wraps). Re-run the measurement (and the morph) on resize if your layout is responsive enough that the target size would meaningfully change.

Yes. Keep a ref to a hidden probe element (or measure a temporarily-rendered off-screen version of your real component) after your data arrives, store the measured size in state, and apply it as inline width/height/border-radius styles on the skeleton element with a CSS transition, swapping in the real component once a timeout matching that transition\u2019s duration completes.