Loaders✨ Premium

Skeleton to Content

Seven shimmer bones (1.8 s) inside a 280 px card — 48 px avatar, 2 header lines, 3 text lines, 100 px image block; one click adds .loaded: bones fade out in 500 ms (opacity + scale(.98)), content slides in 8 px with a 150 ms delay.

CSSJSTransition

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Loaders has 29 effects, including 4 free. Explore the category →

3 usage examples

Pull to refresh — the skeleton inserted at the top of the feed becomes the new post — Skeleton to Content example 1

① Pull to refresh — the skeleton inserted at the top of the feed becomes the new post

WhenOn mobile, the user pulls the feed down and lets go. Instead of a lone spinning ring, a skeleton card is inserted right away at the top of the list, above the posts already on screen, then turns in place into the freshly received post once the request resolves.
WhyThe delicate moment of a pull-to-refresh is the insertion: a spinner alone leaves a gap that jumps once when it disappears and again when the card lands. Here the skeleton holds in advance the exact height of the incoming post — 48 px circle, two header lines, three text lines, image block — and the switch happens inside the same block: the bones fade out over 500 ms (opacity + scale(.98)) while the content takes their place, and the post underneath does not move a pixel. A loader that does not carry both states in the same card cannot guarantee that.
SettingsRemove the button (without #toggleSkelBtn, the delivered script exits without wiring anything: the switch is yours to trigger); set .toggle-skel-wrapper to width: 100% of the mobile column; reduce the image block to 56 px (the bone and .toggle-skel-content-img together) to fit the screen; pin the real text at min-height: 50px = the height of the three bones; add .loaded to #toggleSkelCard in the fetch() callback fired on release.
Dashboard widget — KPI indicator waiting on an API call — Skeleton to Content example 2

② Dashboard widget — KPI indicator waiting on an API call

WhenA key metric or mini-chart waits for a server response before showing the real value. The skeleton occupies the slot on first render and prevents a layout shift.
WhyThe card is anchored at min-height: 200px, keeping the dashboard grid stable during loading. The 500 ms fade is unobtrusive and does not draw attention to the state change.
SettingsReplace the real content with a number and a trend value; reduce the image block height to 60 px; set border-radius: 12px on .toggle-skel-card to match a rounder design system.
Profile preview — card loaded on modal open — Skeleton to Content example 3

③ Profile preview — card loaded on modal open

WhenA “View profile” modal or account popover shows a skeleton while it fetches the user's data, then reveals name, avatar and bio.
WhyThe card is the focal point and the modal opens with something visible on the first frame — no flash of white or empty space. The fade + 8 px slide gives a sense of speed without hiding the real latency.
Settings.toggle-skel-content-avatar: replace the letter “A” with a lazy-loaded <img>; inject text content into the DOM before adding .loaded to #toggleSkelCard.

How it works

The .toggle-skel-card card stacks two layers in the same block: .toggle-skel-bones (the skeleton) and .toggle-skel-content (the real content). The skeleton holds 7 bones — a 48 px circle for the avatar, two header lines (60% and 40% wide), three text lines (100%, 90%, 65%) and a 100 px-tall image rectangle. Each bone uses background: rgba(255,255,255,.08); a ::after pseudo-element animates shimmerSlide — a linear-gradient at 10% opacity sweeping from −200% to 200% over 1.8 s ease-in-out, looping indefinitely. The real content layer shows an indigo-violet gradient avatar (#6366f1#a855f7) with the letter “A”, the name “Alice Durand”, the handle @alice_dev, a paragraph and a dark-blue image block.

The switch relies on the .loaded class being toggled on #toggleSkelCard. On the skeleton side: transition: opacity .5s, transform .5s — when .loaded is present, opacity: 0 and transform: scale(.98) fade the bones out over 500 ms while they also become position: absolute; inset: 20px to free the space. On the content side: display: none at base, display: block as soon as .loaded is set, then transition: opacity .5s .15s, transform .5s .15s — the content enters from 0 to 1 in opacity and from translateY(8px) to 0, both delayed by 150 ms. The button label toggles: “Load content” → “Show skeleton”.

Why the click is wired in JS rather than through an onclick attribute. The delivered script is an IIFE — (function(){ … })() — and everything declared inside it is invisible from the global scope. An onclick="myFunction()" attribute, however, is evaluated in that global scope: a function enclosed in the IIFE does not exist there, and the click would throw a ReferenceError without triggering anything. The delivered code removes the problem at its source: it fetches #toggleSkelCard and #toggleSkelBtn with getElementById, returns immediately if either is missing (if (!card || !btn) return;), then attaches btn.addEventListener('click', …) from inside the IIFE — the listener sees card and btn through its closure, never through window. On each click it runs card.classList.toggle('loaded') and swaps the button label to match the state. The HTML therefore carries no onclick and no global function is created: nothing can collide with another effect pasted on the same page. Consequence for integrators: there is no window.toggleSkeleton() — to trigger the switch from your own code, act on the .loaded class of #toggleSkelCard or click the button programmatically (btn.click()).

Accessibility

  • prefers-reduced-motion not handled in the CSS or JS: the shimmer (1.8 s loop) and the toggle transitions (500 ms) run regardless of system preferences. Measured under reducedMotion: reduce emulation: animationPlayState: running on the bones. Add @media (prefers-reduced-motion: reduce) { .skel-bone::after, .skel-bone-circle::after { animation: none } .toggle-skel-bones, .toggle-skel-content { transition: none } }.
  • No ARIA attributes on the component: no role="status" or aria-live on the card, no aria-busy during the skeleton state. A screen reader has no indication that the card is loading. Integrators should add aria-busy="true" to .toggle-skel-card in skeleton mode, flip it to "false" on state change, and place an aria-live="polite" region outside the card to announce when content is ready.
  • The button is a real <button>: focusable and keyboard-activatable, with a visible text label that the delivered listener updates on every switch (“Load content” / “Show skeleton”). To make the state explicit for assistive technologies, add aria-pressed synchronized with .loaded in that same listener — and keep the label fixed if you do: a “Show skeleton” button announced as “pressed” would be contradictory.
  • Real content is dark-background-only: the content layer uses rgba(255,255,255,…) colors — they are unreadable on a light background. In production, adapt the content colors if your interface supports a light theme.

Browser compatibility

Uses transition, @keyframes, linear-gradient, border-radius, rgba(), getElementById(), addEventListener() and classList.toggle(). No external dependencies, no global variable.

Chrome 26+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS 9+✓ Full
Android Chrome✓ Full

Without JavaScript, .loaded is never added: the skeleton stays visible indefinitely with the shimmer looping. If #toggleSkelCard or #toggleSkelBtn is missing from the DOM, the script exits before attaching the listener, with no console error — same outcome, a permanent skeleton. Without @keyframes (IE 9 unprefixed), the bones display as flat shapes with no shimmer. CSS transitions are ignored under IE 9, so the state change would be instant.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="toggle-skel-wrapper">
  <div class="toggle-skel-card" id="toggleSkelCard">
    <div class="toggle-skel-bones">
      <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
        <div class="skel-bone-circle" style="width:48px;height:48px;flex-shrink:0"></div>
        <div style="flex:1">
          <div class="skel-bone" style="height:14px;width:60%;margin-bottom:8px"></div>
          <div class="skel-bone" style="height:10px;width:40%"></div>
        </div>
      </div>
      <!-- 3 text lines -->
      <div class="skel-bone" style="height:10px;width:100%;margin-bottom:8px"></div>
      <div class="skel-bone" style="height:10px;width:90%;margin-bottom:8px"></div>
      <div class="skel-bone" style="height:10px;width:65%;margin-bottom:16px"></div>
      <!-- image block -->
      <div class="skel-bone" style="height:100px;width:100%;border-radius:10px"></div>
    </div>
    <div class="toggle-skel-content">
      <!-- real content — replace with your data -->
    </div>
  </div>
  <button class="toggle-skel-btn" id="toggleSkelBtn">Load content</button>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Shimmer duration (CSS — animation: 1.8s ease-in-out 0s infinite on ::after) 1.8 s Speed of the light sweep across each bone. 1s for a snappier feel; 2.5s for a softer one. All bones share the same duration and stay in sync.
Skeleton fade-out duration (CSS — transition: opacity .5s, transform .5s on .toggle-skel-bones) .5 s Time the bones take to disappear when .loaded is added. .3s for a faster switch; .8s for a long cross-fade during a slow API response.
Content entry delay (CSS — delay in transition: opacity .5s .15s, transform .5s .15s) .15 s How long the content waits before starting to enter. 0s = content and skeleton move simultaneously; .3s = bones nearly gone before content appears.
Slide distance (CSS — transform: translateY(8px) on .toggle-skel-content) 8 px How far the content travels upward on entry. 4px for a barely noticeable motion; 20px for a more assertive entrance. Switch to translateX(8px) for a lateral entry.
Component width (CSS — .toggle-skel-wrapper { width: 280px }) 280 px All percentage-based bones are relative to this width. Set to 100% to adapt to the parent container without touching the internal proportions.
Image block height (inline HTML — height:100px on the last bone and on .toggle-skel-content-img) 100 px Change both values together so the image bone and its real counterpart share the same height, keeping the layout stable across the switch.
Bone color (CSS — background: rgba(255,255,255,.08)) white at 8% opacity Calibrated for the dark #0a0a0f background. On a light background, switch to rgba(0,0,0,.08); on a tinted background, match the dominant hue at 10–15%.

FAQ

Because the script is an IIFE. An onclick="toggleSkeleton()" attribute resolves its name in the global scope; a function declared inside (function(){ … })() is not there, and the click would throw ReferenceError: toggleSkeleton is not defined without changing the card at all. The delivered code therefore attaches the listener from inside the IIFE, with btn.addEventListener('click', …), where card and btn are reachable through the closure — this is the catalog convention: vanilla, IIFE, no globals, so no risk of collision between two effects pasted on the same page. You can still expose a function (window.toggleSkeleton = function(){ card.classList.toggle('loaded') } inside the IIFE) if your integration requires an onclick, but you give up that isolation; prefer the listener as delivered, or btn.click() from your own code.
The other effects in the category — Skeleton Screen, Card, List, Image Grid, Profile, Article, Dashboard Skeleton — are static loading states: CSS only, looping shimmer, no real content in the HTML, no exit mechanism. Skeleton to Content is the only one that embeds both states in the same block, provides a JS toggle and animates the transition between them. It answers “how do you go from skeleton to real content?” rather than just “what does a skeleton look like?”
There is no global function to call: the switch is the .loaded class on #toggleSkelCard. Simplest route: remove the <button> from the HTML (the delivered script then exits without doing anything — you can even leave it out) and set the class in your callback: fetch('/api/profile').then(r => r.json()).then(data => { /* inject data into .toggle-skel-content */ document.getElementById('toggleSkelCard').classList.add('loaded'); }). Prefer add over toggle: a second call will not flip back to the skeleton. Alternative, if you keep the button hidden: document.getElementById('toggleSkelBtn').click() goes through the delivered listener, which also handles the label. Either way, the script runs on load and looks up its elements immediately — place it after the card's HTML, or inside DOMContentLoaded.