Skip to main content
Gremorie

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 className

Installation

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-skeleton
import { 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.

PropTypeDefaultDescription
classNamestring-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.
...propsReact.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

  1. <Skeleton> is a single element with no children. Shape it with className; nest multiple for compound placeholders (avatar plus name plus secondary line).
  2. 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.
  3. Inside an AspectRatio: pair Skeleton with an AspectRatio to reserve cover-image space without computing dimensions by hand. See AspectRatio.
  4. 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) and aria-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-pulse respects prefers-reduced-motion via the global project override - users who opt out see a static state, not a flashing one.
  • Token-driven surface: bg-accent adapts to light and dark themes automatically. Override to bg-muted when the placeholder needs to sit on a darker card surface.
  • 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.

On this page