Free pack

Skeleton — .mv-skeleton

Loading placeholders with a shimmer synced across the whole page (or a pulse), text, heading, avatar, media and button shapes, and a blurred cross-fade reveal into the loaded content driven by aria-busy.

CategoryFeedback
TypeCSS only (.mv-skeleton)
Statusstable
Keywordsskeleton, placeholder, loading, shimmer, pulse, chargement, reveal

When to use

Avoid when

Install

node scripts/add.mjs skeleton --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["skeleton"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, components/skeleton/skeleton.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,17rem),1fr));gap:1.5rem;width:100%;align-items:start">

  <!-- Card composition (shimmer) -->
  <div aria-hidden="true" style="display:grid;gap:1rem;padding:1.25rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface)">
    <div class="mv-skeleton" data-shape="media"></div>
    <div class="mv-skeleton" data-shape="heading" style="width:70%"></div>
    <div class="mv-skeleton-text">
      <span class="mv-skeleton"></span>
      <span class="mv-skeleton"></span>
      <span class="mv-skeleton"></span>
    </div>
    <div style="display:flex;gap:.5rem">
      <span class="mv-skeleton" data-shape="button"></span>
      <span class="mv-skeleton" data-shape="button" style="width:4.5rem"></span>
    </div>
  </div>

  <!-- List rows (pulse, set on the container) -->
  <div data-skeleton="pulse" aria-hidden="true" style="display:grid;gap:1.1rem;padding:1.25rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface)">
    <div style="display:flex;gap:.75rem;align-items:center"><span class="mv-skeleton" data-shape="circle"></span><div class="mv-skeleton-text" style="flex:1"><span class="mv-skeleton" style="width:55%"></span><span class="mv-skeleton" style="width:80%"></span></div></div>
    <div style="display:flex;gap:.75rem;align-items:center"><span class="mv-skeleton" data-shape="circle"></span><div class="mv-skeleton-text" style="flex:1"><span class="mv-skeleton" style="width:40%"></span><span class="mv-skeleton" style="width:70%"></span></div></div>
    <div style="display:flex;gap:.75rem;align-items:center"><span class="mv-skeleton" data-shape="circle"></span><div class="mv-skeleton-text" style="flex:1"><span class="mv-skeleton" style="width:62%"></span><span class="mv-skeleton" style="width:48%"></span></div></div>
    <div style="display:flex;gap:.75rem;align-items:center"><span class="mv-skeleton" data-shape="square" style="--mv-skeleton-size:2.5rem"></span><div class="mv-skeleton-text" style="flex:1"><span class="mv-skeleton" style="width:50%"></span><span class="mv-skeleton" style="width:75%"></span></div></div>
  </div>

  <!-- Reveal: skeleton cross-fades into the loaded content -->
  <div style="display:grid;gap:.75rem">
    <div class="mv-skeleton-reveal" id="sk-demo-reveal" aria-busy="true" aria-live="polite" style="padding:1.25rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface)">
      <div data-skeleton aria-hidden="true" style="display:grid;gap:1rem">
        <div style="display:flex;gap:.75rem;align-items:center">
          <span class="mv-skeleton" data-shape="circle" style="--mv-skeleton-size:3rem"></span>
          <div class="mv-skeleton-text" style="flex:1"><span class="mv-skeleton" style="width:60%"></span><span class="mv-skeleton" style="width:40%"></span></div>
        </div>
        <div class="mv-skeleton-text"><span class="mv-skeleton"></span><span class="mv-skeleton"></span></div>
      </div>
      <div data-content style="display:grid;gap:1rem">
        <div style="display:flex;gap:.75rem;align-items:center">
          <span style="display:grid;place-items:center;width:3rem;height:3rem;border-radius:50%;background:var(--mv-accent-subtle);color:var(--mv-accent-fg);font-weight:600">CR</span>
          <div style="display:grid;gap:2px"><strong style="font-size:.875rem">Chloe Reed</strong><span style="font-size:.8125rem;color:var(--mv-fg-muted)">Product designer · London</span></div>
        </div>
        <p style="margin:0;font-size:.875rem;line-height:1.55;color:var(--mv-fg-muted)">Working on the customer portal redesign and the new design system.</p>
      </div>
    </div>
    <button class="mv-button" data-variant="outline" data-size="sm" id="sk-demo-reload" style="justify-self:start">Reload</button>
  </div>
</div>
<script type="module">
  const box = document.getElementById("sk-demo-reveal");
  const btn = document.getElementById("sk-demo-reload");
  let timer = 0;
  const load = () => {
    clearTimeout(timer);
    box.setAttribute("aria-busy", "true");
    timer = setTimeout(() => box.setAttribute("aria-busy", "false"), 1800);
  };
  btn?.addEventListener("click", load);
  if (box) load();
</script>

API

Attributes

NameTypeDefaultDescription
data-shapetext | heading | circle | square | button | mediaReady-made shapes (circle/square use --mv-skeleton-size).
data-variantpulse | static(shimmer)Opacity pulse, or no motion.
data-skeleton="pulse"on a containerSwitches every descendant skeleton to pulse.
aria-busytrue | falseOn .mv-skeleton-reveal: loading state.

CSS classes

NameDescription
mv-skeletonLoading block (any width/height, 1rem tall by default).
mv-skeleton-textStack of .mv-skeleton lines; the last one is shorter.
mv-skeleton-revealStacks [data-skeleton] and [data-content]; aria-busy="true" shows the skeleton, "false" reveals the content with a fade + blur.

CSS variables

NameDefaultDescription
--mv-skeleton-baseBlock color (default: text at 8%, adapts to the background).
--mv-skeleton-shineShimmer color.
--mv-skeleton-duration2sDuration of one cycle.
--mv-skeleton-size2.5remSize of the circle / square shapes.

Accessibility

Skeletons are decorative: set aria-hidden="true" on their container, and aria-busy="true" (+ aria-live="polite") on the loading region. The shimmer is synced (background-attachment: fixed) to avoid the flicker of multiple waves. Reduced motion: shimmer and pulse stop, the reveal is instant.