Skeleton
Reserve space for content while it loads.
import { Skeleton } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use Skeleton when you know the shape of the loading content, such as rows, a profile or a card. Use a Spinner for short waits without a known layout, or Progress for measurable work.
A skeleton is a block. Size and round it with classes to match the content it stands in for.
import { Skeleton } from '@youj/react'
export function ProfileSkeleton() {
return (
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-full" />
<div className="grid flex-1 gap-2">
<Skeleton className="h-3.5 w-3/5" />
<Skeleton className="h-3 w-2/5" />
</div>
</div>
)
}Description
A Members list that loads for about two seconds and resolves into content; Reload runs it again.
Pair skeletons with motion's Content Swap, as the demo does. The skeleton drops away quickly, the content resolves into focus a step later, and the box morphs between the two heights, so the page below moves instead of jumping. ContentSwap also marks the region aria-busy and announces the wait once. Press Reload to watch it go both ways.
import { ContentSwap } from '@youj/motion'
<ContentSwap loading={isLoading} fallback={<MembersSkeleton />} loadingLabel="Loading members">
<MemberList members={data} />
</ContentSwap>Skeletons for a profile card and other shapes, sized to the content they stand in for.
Match the real content's geometry: the avatar's size, the line heights, the width a title usually takes. Vary line widths so a paragraph reads as text rather than a grid of bars.
Stat tiles that each resolve when their own request answers.
When a view fills in from several requests, give each region its own skeleton and ContentSwap, so each one resolves as soon as its data arrives instead of waiting for the slowest. Anything you know before the data, like these labels, stays real. Only the numbers wait.
Code
skeleton-demo.tsx, the Demo story. The component itself comes from the package.
Reference
PropsOptions
Props
Skeleton adds no props of its own. They pass through to ComponentProps<'div'>.
MotionShimmer · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Shimmera band of light crosses in the first 60% of each cycle, then rests | duration.deliberateease.standard560ms · cubic-bezier(0.2, 0, 0, 1) · × 3 per cycle | 560mscubic-bezier(0.2, 0, 0, 1)× 3 per cycle |
Reduced motion
The band is gone and the blocks hold still.
The cycle is three deliberate beats, so the band reads as a calm pulse rather than a constant wash. It follows the Speed control, and it runs the other way under right-to-left.
Reduced motion
The traveling band is removed and each block is a still, recessed shape. The layout still shows where content will land.
AccessibilityARIA, announcements
Accessibility
- Skeletons are
aria-hidden: a pile of empty blocks means nothing to a screen reader. - Put
aria-busyon the region they stand in for, and announce the wait once in a polite status, such as "Loading members". ContentSwap does both withloadingLabel. - Don't leave skeletons up once the content has failed. Swap them for an error with a way to retry.