Skeleton
Pulsing placeholder block that reserves layout space while content fetches.
Overview
Skeleton is the loading-placeholder primitive: a pulsing block you shape with width and height to match the geometry of the real content underneath. The point is not the animation - it is reserving the layout slot so there is no shift when the data arrives.
Reach for Skeleton on every async surface where the layout depends on the response: avatar plus name plus secondary text, a card cover, a row in a list, a chart. Skip it for in-flow operations with known duration - use Progress there - and for short button-press pending states - use a Spinner inside the button.
Preview
'use client';import { Skeleton } from '@gremorie/rx-feedback';export function SkeletonPreview() { return ( <div className="flex items-center gap-4"> <Skeleton className="size-12 rounded-full" /> <div className="flex flex-col gap-2"> <Skeleton className="h-4 w-[200px]" /> <Skeleton className="h-4 w-[160px]" /> </div> </div> );}Anatomy
Skeleton a single pulsing, rounded block sized by its classNameInstallation
bash npx gremorie@latest add rx-skeleton bash pnpm dlx gremorie@latest add rx-skeleton bash yarn dlx gremorie@latest add rx-skeleton bash bunx --bun gremorie@latest add rx-skeleton Usage
import { Skeleton } from "@gremorie/rx-feedback";
export function ProfileSkeleton() {
return (
<div className="flex items-center gap-4" aria-busy="true" aria-live="polite">
<Skeleton className="size-12 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-4 w-[160px]" />
</div>
</div>
);
}npx gremorie@latest add ng-skeletonimport { Component } from '@angular/core';
import { Skeleton } from '@gremorie/ng-feedback';
@Component({
selector: 'app-profile-skeleton',
standalone: true,
imports: [Skeleton],
template: `
<div class="flex items-center gap-4" aria-busy="true" aria-live="polite">
<gr-skeleton class="size-12 rounded-full" />
<div class="flex flex-col gap-2">
<gr-skeleton class="h-4 w-[200px]" />
<gr-skeleton class="h-4 w-[160px]" />
</div>
</div>
`,
})
export class ProfileSkeletonComponent {}API
<Skeleton>
Renders a <div> styled with animate-pulse rounded-md bg-accent. Shape the element with width and height utility classes that match the content underneath.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | The whole API. Set width and height (size-12, h-4 w-[200px]), override the radius (rounded-full for avatars), or change the surface (bg-muted) when the default accent surface clashes with the parent. |
...props | React.ComponentProps<"div"> | - | Standard div attributes. The component itself adds no role; semantics belong to the surrounding region. |
The default animation uses Tailwind's animate-pulse. Users with
prefers-reduced-motion: reduce automatically get the static state - the
project ships a global motion override, no per-component config required.
Composition
<Skeleton>is a single element with no children. Shape it withclassName; nest multiple for compound placeholders (avatar plus name plus secondary line).- Match the real geometry: a 12 px circle, a 16 px text line, a 200 px column. The closer the placeholder shape is to the final content, the less the user notices when it swaps in.
- Inside an
AspectRatio: pair Skeleton with anAspectRatioto reserve cover-image space without computing dimensions by hand. See AspectRatio. - Per-list-row vs whole-card: render one Skeleton row per expected item count rather than a single big block - row-grain placeholders look more honest and stop flickering when the data lands progressively.
Variations
Avatar plus text
'use client';import { Skeleton } from '@gremorie/rx-feedback';export function SkeletonAvatarPreview() { return ( <div className="flex items-center gap-4" aria-busy="true" aria-live="polite" > <Skeleton className="size-12 rounded-full" /> <div className="flex flex-col gap-2"> <Skeleton className="h-4 w-[200px]" /> <Skeleton className="h-4 w-[160px]" /> </div> </div> );}The standard user-profile placeholder: a circular avatar beside two short text lines. Two lines feel honest; three is the upper bound before the placeholder starts to flicker on real data.
Card placeholder
'use client';import { Skeleton } from '@gremorie/rx-feedback';export function SkeletonCardPreview() { return ( <div className="max-w-sm space-y-3 rounded-md border p-4" aria-busy="true" aria-live="polite" > <Skeleton className="aspect-video w-full rounded-md" /> <Skeleton className="h-4 w-3/4" /> <Skeleton className="h-4 w-1/2" /> </div> );}For card-grid layouts. Use aspect-video on the cover Skeleton so the placeholder height matches what the real <img> will reserve, then taper the two text lines to w-3/4 and w-1/2 so the block reads as a heading plus subtext.
List rows
'use client';import { Skeleton } from '@gremorie/rx-feedback';export function SkeletonListPreview() { return ( <ul className="flex w-full max-w-md flex-col gap-3" aria-busy="true" aria-live="polite" > {Array.from({ length: 5 }).map((_, index) => ( <li key={index} className="flex items-center gap-3"> <Skeleton className="size-8 rounded-full" /> <Skeleton className="h-3 flex-1" /> </li> ))} </ul> );}Render one row per expected item rather than a single big block. Use a fixed count that matches the typical page size (5-10 rows). Overshooting feels like fake throughput; undershooting causes a jump when more rows arrive.
Accessibility
- Presentation only: Skeleton itself renders no role and no label. It is a visual placeholder.
- Mark the loading region: wrap the placeholders in a container with
aria-busy="true"(the region is loading) andaria-live="polite"(announce the swap when content arrives) so screen readers get a single announcement instead of repeated chatter on each pulsing element. - Reduced motion:
animate-pulserespectsprefers-reduced-motionvia the global project override - users who opt out see a static state, not a flashing one. - Token-driven surface:
bg-accentadapts to light and dark themes automatically. Override tobg-mutedwhen the placeholder needs to sit on a darker card surface.
Related
- Progress - reach for Progress when percent complete is known.
- Alert - reach for Alert when the surface needs to explain why the user is waiting.
- AspectRatio - pair with Skeleton to reserve cover-image space without computing dimensions by hand.