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>

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

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

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than assuming this is just a fancier crossfade, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how measureNaturalSize() gets an accurate size for content that hasn't been shown yet by rendering a hidden off-screen probe, and why the forced reflow via offsetWidth before the next morph is necessary to avoid the browser silently skipping the transition. The same assistant can help optimize it — for instance asking whether the off-screen probe element should be cached and reused across morphs instead of created and destroyed each time, to reduce layout thrashing. It's also useful for extending it: ask it to make the morph responsive to viewport resizes by re-measuring and re-animating, add a subtle overshoot/bounce easing to the resize transition, or generalize morphBlock() into a small reusable utility that takes any skeleton element and any real-content renderer function. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a skeleton loading state in plain HTML, CSS, and JavaScript — no libraries — where the skeleton blocks genuinely MORPH their shape into the real content's actual size, rather than an instant swap or a same-size opacity crossfade.

Requirements:
- A small card containing at least three skeleton placeholder blocks of different kinds: a circular avatar placeholder, a short title-bar placeholder, and a taller multi-line body-text placeholder, each with a shimmer gradient animation while in the skeleton state.
- Before revealing each block's real content, measure that real content's TRUE rendered size by creating a hidden, off-screen probe element (positioned far outside the viewport, not just display:none) with the same content and styling the real element will have, reading its getBoundingClientRect() width and height, then removing the probe from the DOM.
- Animate the visible skeleton block's own inline width, height, and border-radius styles directly to that measured target size using a CSS transition on those exact properties — the block must visibly grow or shrink into the real content's footprint, not just fade at a fixed size.
- Only after that resize transition has had time to finish should the real content (avatar image/initials, real name text, real body text) actually render inside the block, itself fading in with its own opacity transition — a clear two-phase sequence, not both changes happening simultaneously.
- Include a "reload" button that resets all blocks back to their generic skeleton size and re-runs the whole measure-then-morph sequence, using a forced synchronous reflow (reading an offsetWidth/offsetHeight property) between the reset and the next size change so the transition is never accidentally skipped by the browser batching style changes.
- Make the real content's text (a name and a short bio) different lengths from a naive default skeleton size, so the morph is visually obvious rather than a no-op.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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.