Wraps any content and turns it into its own shimmering skeleton while
loading is true. The component applies a transparent text + animated
gradient background to every descendant, so the placeholder naturally follows
the shape of whatever children you pass — headings, paragraphs, buttons,
avatars, cards.
The technique uses color: transparent, a horizontally animated
linear-gradient and -webkit-box-decoration-break: clone so multi-line text
shimmers per line.
While loading, the root element receives aria-busy="true", aria-live="polite"
and inert, so keyboard focus cannot land inside the skeleton.
#Which variant, and when
Two loading semantics, picked with the variant prop:
skeleton (default) — you don’t have the content yet. Every descendant
is replaced with shimmer so the user sees the shape of what’s loading.
Reach for this on first render, after a navigation, or when the content
structure is known but the data isn’t.
busy — you already have the content, but an action is in flight
(saving a form, refreshing a feed, running a search). Children stay
legible and simply blur with a gentle opacity pulse. Users keep their
context, they just can’t interact until it resolves.
Rule of thumb: if the user has never seen this content before, use
skeleton. If they’re waiting for the same content to update, use busy.
#Picking the wrapper element
Loading renders its root with display: contents, so the wrapper disappears
from layout and the children sit directly in their natural flow. Wrap at the
granularity you want the loading state to cover:
- Wrapping a single card gives you a card-level skeleton.
- Wrapping a list wrapper gives you a list-level skeleton — each row already
composes into its own shape.
- Wrapping a form lets you show
busy across the whole form including its
submit button.
#Per-child overrides
For the rare mixed cases, drop data-loading-as on any descendant to change
the treatment locally:
data-loading-as="skeleton" — force skeleton on this subtree even if the
ancestor is busy (e.g. a stat number that’s still being computed while the
rest of the form blurs).
data-loading-as="busy" — force busy on this subtree inside a skeleton
(e.g. a persistent “Saving…” button that stays readable).
data-loading-as="none" — opt a subtree out entirely (e.g. a progress bar
or spinner that drives the loading state itself).
#Component-specific behaviour
The recipe recognises Pindoba components via their data-component attribute
and gives each the right treatment:
- Badge, Stamp, Checkbox, Radio — shimmered as atomic blocks so their
pill / circle / square shape is preserved; inner text and icons are hidden.
- Button — in
skeleton the whole button becomes a shimmer block (no
label); in busy the button keeps its label but blurs and pulses.
- Input, Select, Textarea — the padded wrapper shimmers edge-to-edge and
the inner control + placeholder are hidden, so no stray placeholder text
leaks through.
- Card, Alert, Banner, and regular text nodes — follow the generic leaf
shimmer, so their internal spans and paragraphs animate line-by-line.
#Accessibility
The root receives aria-busy="true", aria-live="polite" and inert while
loading, so assistive tech is notified and keyboard focus can’t land inside a
placeholder. The shimmer and pulse animations respect prefers-reduced-motion
and collapse to a static state.
#Default
Toggle the loading state — the same markup serves as both the real content and
its skeleton. Wrapping text in a <span> lets each wrapped line shimmer
individually.
#Card
A profile card composed of Card, Badge and Button. Everything inside the
card becomes a skeleton without any extra markup.
#Banner & Alerts
Loading works seamlessly over messaging components like Banner and Alert —
icons are hidden while their containing boxes still shimmer.
Skeletonize form fields (Input, Select) and their submit actions to
indicate a pending save or fetch.
#Feed
A feed layout with Stamp avatars, Badge tags and Button actions. Each
post becomes its own composed skeleton.
#Props
| prop | type | default | req | description |
| loading | boolean | false |
·
| When true, renders children as a shimmering skeleton. |
| variant | "skeleton""busy" | "skeleton" |
·
| Loading treatment. `skeleton` replaces content with shimmer. `busy` blurs + pulses the existing content. |
| speed | "slow""normal""fast" | "normal" |
·
| Shimmer animation speed. |
| radius | "sm""md""pill" | "sm" |
·
| Border radius applied to each shimmering descendant. |
| colorPalette | string | "neutral" |
·
| Tints the skeleton shimmer using any palette token. |
| passThrough | { root?: { style?: SystemStyleObject; props?: HTMLAttributes<HTMLElement> } } | undefined |
·
| Custom styling and props for the loading root element. |
| children | Snippet | undefined |
·
| Content rendered normally, or as a skeleton when loading. |
| ...rest | HTMLAttributes<HTMLDivElement> | - |
·
| Standard HTML div attributes. |