A skeleton whose geometry is the content's geometry, so the arrival is a crossfade in place rather than a rectangle swapped for real content and everything jumping.
Alexandra Rodriguez
Principal Engineer
Runs the design systems team. Previously at two payments companies. Occasional conference speaker, full-time dog owner.
pnpm dlx shadcn@latest add https://matos-ui.com/r/skeleton-morph.jsonimport { SkeletonMorph, Skel } from '@/components/matos-ui/skeleton-morph'<SkeletonMorph loading={query.isPending}>
<div className="flex items-center gap-3">
<Skel className="size-12 rounded-full">
<Avatar src={user.avatar} />
</Skel>
<div className="space-y-1.5">
<Skel className="h-5 w-40">
<p className="truncate font-medium text-sm">{user.name}</p>
</Skel>
<Skel className="h-4 w-28">
<p className="truncate text-muted-foreground text-xs">{user.role}</p>
</Skel>
</div>
</div>
</SkeletonMorph>You lay the screen out once and wrap each data-bound piece in a Skel. While
loading, each Skel shows a shimmer of the exact size and radius of its final
content; when the query resolves, the shimmer crossfades out and the content
crossfades in — in the same place.
The registry's plain skeleton is a rectangle — the content arrives and everything shifts, which makes the interface look fast without being fast. Matching the shape removes the shift.
Skel's className is the box — the shimmer's, and the frame the
content is clipped to. Size it to match the content (h-5 w-40,
size-10 rounded-full, aspect-[2/3]) and clamp the content to fit
(truncate, line-clamp-3, object-cover).layout on the root settles as a morph, not a jump.Nothing here elevates. The shimmer fill is an alpha of the foreground, so a
Skel reads on whatever rung its content sits on — the same offset as the
content. That is the requirement, not a detail: a skeleton one rung off from
its content is the jump it was supposed to prevent.
fast — it responds.prefers-reduced-motion the shimmer is dropped entirely. It is
ambient decoration, and the rule is to kill ambient decoration, not necessary
motion — the flat fill still marks the box, and the crossfade still runs.A Skel's own loading prop overrides the boundary's. A lone Skel with
loading and no SkeletonMorph around it is a permanent shimmer box — the cover
fallback in Cover Image, where the image is never
coming.
<Skel loading className="aspect-[2/3] rounded-lg" />| Prop | Type | Default | Description |
|---|---|---|---|
loading | boolean | — | While true, every Skel inside shows its shimmer. |
children | ReactNode | — | Your layout, with a Skel around each data piece. |
as | "div" | "span" | "div" | Root element. |
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | The box: the shimmer's size and radius, and the content frame. |
children | ReactNode | — | The real content. |
loading | boolean | context | Overrides the boundary's loading. |
as | "span" | "div" | "span" | Use "div" for a block-level region. |
Install Matos UI
Choose a package manager and copy one command for every component.