Skeleton Morph
A copy-paste view transitions component in pure HTML, CSS & vanilla JS. Zero dependencies, framework-agnostic, MIT-licensed.
View TransitionsHTMLJavaScriptCSSany framework
This is the loaded headline.
Loading
Copy into your project
HTML
<div class="nuda-vt2-skeleton-morph">
<div class="nuda-vt2-skeleton-morph__card" data-state="loading">
<span class="nuda-vt2-skeleton-morph__bar nuda-vt2-skeleton-morph__bar--wide"></span>
<span class="nuda-vt2-skeleton-morph__bar"></span>
<p class="nuda-vt2-skeleton-morph__text">This is the loaded headline.</p>
</div>
<button type="button" class="nuda-vt2-skeleton-morph__btn">Load content</button>
<p class="nuda-vt2-skeleton-morph__status" aria-live="polite">Loading</p>
</div>JavaScript
/* Skeleton Morph — vanilla JS
Crossfades a shimmering skeleton into its loaded content inside a short
view transition, and announces the state change for screen readers. */
(function () {
document.querySelectorAll(".nuda-vt2-skeleton-morph").forEach(function (root) {
var card = root.querySelector(".nuda-vt2-skeleton-morph__card");
var status = root.querySelector(".nuda-vt2-skeleton-morph__status");
var btn = root.querySelector(".nuda-vt2-skeleton-morph__btn");
function doSwap() {
var next = card.dataset.state === "loading" ? "loaded" : "loading";
card.dataset.state = next;
status.textContent = next === "loaded" ? "Content loaded" : "Loading";
}
btn.addEventListener("click", function () {
if (!document.startViewTransition) { doSwap(); return; }
document.startViewTransition(doSwap);
});
});
})();CSS
.nuda-vt2-skeleton-morph-demo {
padding: .5rem;
}
.nuda-vt2-skeleton-morph {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.nuda-vt2-skeleton-morph__card {
display: flex;
flex-direction: column;
gap: 6px;
width: 150px;
min-height: 56px;
padding: 10px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,.1);
background: #18181b;
view-transition-name: vt2-skeleton-card;
}
.nuda-vt2-skeleton-morph__bar {
display: none;
height: 10px;
border-radius: 5px;
width: 60%;
background: linear-gradient(90deg,#27272a 25%,#3f3f46 37%,#27272a 63%);
background-size: 400% 100%;
animation: _nuda-vt2shimmer 1.4s ease-in-out infinite;
}
.nuda-vt2-skeleton-morph__bar--wide {
width: 90%;
}
.nuda-vt2-skeleton-morph__card[data-state="loading"] .nuda-vt2-skeleton-morph__bar {
display: block;
}
.nuda-vt2-skeleton-morph__text {
display: none;
margin: 0;
font-size: .74rem;
font-weight: 700;
color: #e4ff54;
}
.nuda-vt2-skeleton-morph__card[data-state="loaded"] .nuda-vt2-skeleton-morph__text {
display: block;
}
.nuda-vt2-skeleton-morph__btn {
background: #e4ff54;
color: #09090b;
border: none;
border-radius: 8px;
padding: .4rem .8rem;
font-size: .75rem;
font-weight: 700;
cursor: pointer;
}
.nuda-vt2-skeleton-morph__btn:focus-visible {
outline: 2px solid #e4ff54;
outline-offset: 2px;
}
.nuda-vt2-skeleton-morph__status {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
@keyframes _nuda-vt2shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: 0 0;
}
}
::view-transition-old(vt2-skeleton-card),::view-transition-new(vt2-skeleton-card) {
animation-duration: .25s;
animation-timing-function: ease-out;
}
@media (prefers-reduced-motion:reduce) {
.nuda-vt2-skeleton-morph__bar {
animation: none;
background-position: 0 0;
}
::view-transition-old(vt2-skeleton-card),::view-transition-new(vt2-skeleton-card) {
animation: none;
}
}
How to use Skeleton Morph
Paste the HTML where you need it and the CSS into a global stylesheet (or a <style> tag). Every class is prefixed nuda- so it never collides with Tailwind or your own styles. Tweak the CSS custom properties to match your design system.
Works in React, Vue, Svelte, Astro, Next.js, Nuxt, Laravel Blade, Django, Rails — or a single .html file. No npm install, no build step.