Exklusiv Beta
Skeleton Morph <mv-skeleton-morph>
Das Skeleton macht dem Inhalt nicht Platz, es wird zu ihm: Jeder Block ist einem echten Element zugeordnet (data-skeleton-for, dann Art, Reihenfolge und Geometrie) und wandert zu ihm. Ein Textbalken erreicht seine echte Zeile, nimmt genau deren Breite an und zieht sich dann zurück, während sich der Text dahinter selbst tippt; ein Kreis verwandelt sich in einen Avatar, ein Rechteck in ein Bild oder einen Button. Funktioniert mit resolve(), einem <template> oder von einem Framework ausgetauschten Kindelementen und kann rückwärts abspielen.
Kategorie Feedback Typ Web Component (<mv-skeleton-morph>) Status Beta Installiert auch skeleton Keywords exclusive, skeleton, loading, morph, flip, shared-element, placeholder, chargement, transition, async
When to use
Loaded content should visibly grow out of its placeholders, each block traveling to its real element A showcase or premium app wants text to type in and avatars or images to morph from their skeletons Content swapped by a framework or a template should transition smoothly from its loading state
Avoid when
Many lists load at once or loading is frequent; plain placeholders with a fade are cheaper → use Skeleton instead The placeholder layout differs a lot from the real content, so blocks have nothing to match
Hol dir Skeleton Morph. In jedem Plan enthalten: Das Pro pack liefert alle 317 Komponenten mit der CLI und dem unten verwendeten MCP-Server.
Pläne ansehen
Installation
node scripts/add.mjs skeleton-morph --out ./src/marvelous
KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["skeleton-morph"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/skeleton-morph/skeleton-morph.js, components/skeleton-morph/skeleton-morph.css, components/skeleton/skeleton.css.
Verwendung
Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div id="sm-demo" class="sm-demo" lang="en">
<style>
.sm-demo { width: min(100%, 62rem); margin-inline: auto; display: grid; gap: var(--mv-space-4); font-family: var(--mv-font-sans); }
.sm-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--mv-space-3); flex-wrap: wrap; }
.sm-bar p { margin: 0; font-size: var(--mv-text-sm); color: var(--mv-fg-muted); }
.sm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: var(--mv-space-4); align-items: start; }
.sm-card { background: var(--mv-surface); border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); padding: var(--mv-space-5); box-shadow: var(--mv-shadow-sm); }
.sm-card > h3 { margin: 0 0 var(--mv-space-4); font-size: var(--mv-text-sm); font-weight: var(--mv-weight-semibold); color: var(--mv-fg-muted); letter-spacing: .02em; }
.sm-post { display: grid; gap: var(--mv-space-4); }
.sm-head { display: flex; align-items: center; gap: var(--mv-space-3); }
.sm-avatar { width: 2.75rem; height: 2.75rem; border-radius: 50%; flex: none; }
.sm-who { flex: 1; min-width: 0; display: grid; gap: .25rem; }
.sm-name { font-size: var(--mv-text-base); font-weight: var(--mv-weight-semibold); line-height: 1.2; }
.sm-meta { font-size: var(--mv-text-xs); color: var(--mv-fg-muted); line-height: 1.2; }
.sm-body { margin: 0; font-size: var(--mv-text-sm); line-height: 1.6; color: var(--mv-fg); }
.sm-media { height: 8.5rem; border-radius: var(--mv-radius-lg); overflow: hidden; position: relative;
background: linear-gradient(135deg, color-mix(in oklch, var(--mv-accent) 22%, var(--mv-surface)), color-mix(in oklch, var(--mv-info) 18%, var(--mv-surface))); }
.sm-media svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.sm-foot { display: flex; align-items: center; gap: var(--mv-space-2); }
.sm-foot .sm-stats { flex: 1; font-size: var(--mv-text-xs); color: var(--mv-fg-muted); }
.sm-rows { display: grid; }
.sm-row { display: grid; grid-template-columns: 1fr auto 5.5rem 4.75rem; align-items: center; gap: var(--mv-space-3); padding-block: .7rem; border-top: 1px solid var(--mv-border); font-size: var(--mv-text-sm); }
.sm-row:first-child { border-top: 0; padding-top: 0; }
.sm-row .sm-ref { color: var(--mv-fg-muted); font-family: var(--mv-font-mono); font-size: var(--mv-text-xs); }
.sm-row .sm-amt { text-align: right; font-variant-numeric: tabular-nums; font-weight: var(--mv-weight-medium); }
.sm-row .mv-badge { justify-self: end; }
.sm-row .mv-skeleton { height: .8rem; }
</style>
<div class="sm-bar">
<p>Skeleton blocks become the content: each bar travels to its real line, takes its exact width, then the text types itself out behind it.</p>
<button class="mv-button" data-variant="outline" type="button" data-reload disabled>Reload</button>
</div>
<div class="sm-grid">
<article class="sm-card">
<h3>News feed</h3>
<mv-skeleton-morph data-feed>
<div class="sm-post">
<div class="sm-head">
<span class="mv-skeleton" data-shape="circle" data-skeleton-for="avatar" style="--mv-skeleton-size:2.75rem"></span>
<div class="sm-who">
<span class="mv-skeleton" style="width:42%;height:.9rem"></span>
<span class="mv-skeleton" style="width:68%;height:.7rem"></span>
</div>
<span class="mv-skeleton" data-shape="button" style="width:4.25rem;height:1.375rem;border-radius:var(--mv-radius-sm)"></span>
</div>
<div class="mv-skeleton-text" style="gap:.7rem">
<span class="mv-skeleton"></span><span class="mv-skeleton"></span><span class="mv-skeleton" style="width:48%"></span>
</div>
<span class="mv-skeleton" data-shape="media" data-skeleton-for="media" style="aspect-ratio:auto;height:8.5rem"></span>
<div class="sm-foot">
<span class="mv-skeleton" style="width:9.5rem;height:.7rem;margin-right:auto"></span>
<span class="mv-skeleton" data-shape="button" style="width:4.5rem;height:2rem"></span>
<span class="mv-skeleton" data-shape="button" style="width:6.25rem;height:2rem"></span>
</div>
</div>
<template data-content>
<div class="sm-post">
<div class="sm-head">
<img class="sm-avatar" data-morph="avatar" alt="" src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 44 44'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='%23f59e0b'/><stop offset='1' stop-color='%23db2777'/></linearGradient></defs><rect width='44' height='44' fill='url(%23g)'/><text x='22' y='28' text-anchor='middle' font-family='Segoe UI,Arial,sans-serif' font-size='16' font-weight='700' fill='white'>MP</text></svg>">
<div class="sm-who">
<strong class="sm-name">Maya Patel</strong>
<span class="sm-meta">Head of Product at Northwind · 2h ago</span>
</div>
<span class="mv-badge" data-variant="secondary">Following</span>
</div>
<p class="sm-body">Our new online refund flow is live: three screens instead of nine, and average turnaround down from 4 days to 36 hours. Huge thanks to the whole team for these six weeks!</p>
<div class="sm-media" role="img" aria-label="Refund turnaround cut by two-thirds between January and September" data-morph="media">
<svg viewBox="0 0 400 136" preserveAspectRatio="none" aria-hidden="true">
<g fill="currentColor" style="color:color-mix(in oklch, var(--mv-accent) 70%, var(--mv-fg))">
<rect x="30" y="26" width="34" height="96" rx="5" opacity=".35"/><rect x="84" y="36" width="34" height="86" rx="5" opacity=".4"/>
<rect x="138" y="44" width="34" height="78" rx="5" opacity=".45"/><rect x="192" y="60" width="34" height="62" rx="5" opacity=".55"/>
<rect x="246" y="78" width="34" height="44" rx="5" opacity=".7"/><rect x="300" y="92" width="34" height="30" rx="5" opacity=".85"/>
<rect x="354" y="100" width="34" height="22" rx="5"/>
</g>
</svg>
</div>
<div class="sm-foot">
<span class="sm-stats">128 reactions · 24 comments</span>
<button class="mv-button" data-variant="ghost" data-size="sm" type="button">Like</button>
<button class="mv-button" data-variant="outline" data-size="sm" type="button">Comment</button>
</div>
</div>
</template>
</mv-skeleton-morph>
</article>
<article class="sm-card">
<h3>Recent orders</h3>
<mv-skeleton-morph data-orders>
<div class="sm-rows">
<div class="sm-row"><span class="mv-skeleton" style="width:62%"></span><span class="mv-skeleton" style="width:2.5rem"></span><span class="mv-skeleton" style="width:4rem;justify-self:end"></span><span class="mv-skeleton" data-shape="button" style="width:3.75rem;height:1.375rem;justify-self:end"></span></div>
<div class="sm-row"><span class="mv-skeleton" style="width:48%"></span><span class="mv-skeleton" style="width:2.5rem"></span><span class="mv-skeleton" style="width:3.5rem;justify-self:end"></span><span class="mv-skeleton" data-shape="button" style="width:3.75rem;height:1.375rem;justify-self:end"></span></div>
<div class="sm-row"><span class="mv-skeleton" style="width:70%"></span><span class="mv-skeleton" style="width:2.5rem"></span><span class="mv-skeleton" style="width:4.25rem;justify-self:end"></span><span class="mv-skeleton" data-shape="button" style="width:3.75rem;height:1.375rem;justify-self:end"></span></div>
<div class="sm-row"><span class="mv-skeleton" style="width:55%"></span><span class="mv-skeleton" style="width:2.5rem"></span><span class="mv-skeleton" style="width:3.75rem;justify-self:end"></span><span class="mv-skeleton" data-shape="button" style="width:3.75rem;height:1.375rem;justify-self:end"></span></div>
<div class="sm-row"><span class="mv-skeleton" style="width:64%"></span><span class="mv-skeleton" style="width:2.5rem"></span><span class="mv-skeleton" style="width:4rem;justify-self:end"></span><span class="mv-skeleton" data-shape="button" style="width:3.75rem;height:1.375rem;justify-self:end"></span></div>
</div>
<template data-content>
<div class="sm-rows">
<div class="sm-row"><span>Olivia Brooks</span><span class="sm-ref">#4821</span><span class="sm-amt">$86.40</span><span class="mv-badge" data-variant="success">Paid</span></div>
<div class="sm-row"><span>Kenji Tanaka</span><span class="sm-ref">#4822</span><span class="sm-amt">$1,249.00</span><span class="mv-badge" data-variant="warning">Pending</span></div>
<div class="sm-row"><span>Amara Okafor-Lind</span><span class="sm-ref">#4823</span><span class="sm-amt">$32.90</span><span class="mv-badge" data-variant="success">Paid</span></div>
<div class="sm-row"><span>Lucas Silva</span><span class="sm-ref">#4824</span><span class="sm-amt">$410.00</span><span class="mv-badge" data-variant="danger">Declined</span></div>
<div class="sm-row"><span>Aisha Rahman</span><span class="sm-ref">#4825</span><span class="sm-amt">$74.50</span><span class="mv-badge" data-variant="success">Paid</span></div>
</div>
</template>
</mv-skeleton-morph>
</article>
</div>
</div>
<script type="module">
const root = document.getElementById("sm-demo");
await customElements.whenDefined("mv-skeleton-morph");
const morphs = [...root.querySelectorAll("mv-skeleton-morph")];
const btn = root.querySelector("[data-reload]");
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
// Fake network: the feed answers first, the orders a little later.
const load = async () => {
btn.disabled = true;
await Promise.all(morphs.map(async (m, i) => { await wait(650 + i * 260); await m.resolve(); }));
btn.disabled = false;
};
btn.addEventListener("click", async () => {
btn.disabled = true;
await Promise.all(morphs.map((m) => m.reset()));
await wait(700);
load();
});
load();
</script>
API
Attributes
Name Typ Default Description durationnumber 1000Duration of one block (ms), excluding stagger. staggernumber 45Delay between successive blocks (ms), in reading order. loading-labelstring Loading…Accessible status text while loading. loaded-labelstring Optional announcement once the content has arrived (e.g. “5 orders loaded”). data-skeleton-foron a skeleton block Name (or selector .x / #x / [x]) of the real element it should become; pairs with data-morph="name" on the content side. Optional: without it, pairing uses kind, order and position. data-morphon a content element Names a target, or forces an element (div role=img, card…) to be treated as a block.
Properties
Name Typ Description loadingboolean Read-only: the skeleton is showing. morphingboolean Read-only: a morph is in progress.
Methods
Name Description resolve(content?) → PromiseMorphs the skeleton into content: Node, DocumentFragment, array of nodes, <template>, or nothing to use <template data-content>. Never an HTML string. reset(skeleton?) → PromiseReverse morph: the text erases under the bars as they return to the initial skeleton (or <template data-skeleton>, or the one provided).
Events
Name Description mv-morphstartdetail: { direction: "in" | "out" }. mv-morphenddetail: { direction }.
Content structure
Name Description (children)The skeleton (.mv-skeleton blocks, .mv-skeleton-text groups), then the content. If a framework swaps the children itself, the change is detected and the morph starts from the skeleton’s remembered positions. template[data-content] / template[data-skeleton]Content revealed by resolve() with no argument; skeleton restored by reset().
CSS classes
Name Description mv-skeleton-morph-layer / -ghostLayer and traveling blocks (data-kind line | block | lone, data-typing while typing).
CSS variables
Name Default Description --mv-skeleton-morph-caretvar(--mv-accent)Color of the thin caret at the edge of a bar that is typing.
Accessibility
aria-busy="true" while loading, skeleton blocks are aria-hidden; an internal role=status exposes loading-label, then loaded-label if provided. The real content is inserted as soon as the morph starts (readable and focusable immediately); only its painting is hidden during the animation. Reduced motion, element off screen or hidden tab: instant swap, no animation. No HTML string is ever parsed.