Sora UI

Skeleton

A loading placeholder with shimmer or fade variants, plus a transition that crossfades into real content once loading finishes.

Made by Axyl

A cover image (fade) above a profile row (shimmer) that crossfades into content once ready.

Loading...

Installation

File Structure

skeleton.tsx

Usage

Basic

import { Skeleton } from '@/components/sora-ui/effects/skeleton';

export default function Page() {
  return <Skeleton className="h-4 w-40" />;
}

Use SkeletonAvatar, SkeletonText, and SkeletonButton for common shapes — they accept all Skeleton props (SkeletonAvatar/SkeletonButton fix rounded to their own shape, so that one prop isn't available on those two):

import {
  SkeletonAvatar,
  SkeletonButton,
  SkeletonText,
} from '@/components/sora-ui/effects/skeleton';

export default function Page() {
  return (
    <div className="flex items-center gap-3">
      <SkeletonAvatar />
      <SkeletonText className="w-32" />
      <SkeletonButton />
    </div>
  );
}

ShimmerSkeleton is an alias for Skeleton (same component, same props) — pick whichever name reads better at the call site.

Variants

variant="shimmer" (default) sweeps a moving highlight; variant="fade" uses Tailwind animate-pulse for a calmer opacity pulse — a good fit for large blocks like cover images or cards:

import { Skeleton } from '@/components/sora-ui/effects/skeleton';

export default function Page() {
  return <Skeleton className="h-24 w-full" rounded="lg" variant="fade" />;
}

Customizing colors

There's no color or background prop — Skeleton reads two CSS variables instead: --sk-muted for the surface fill and --sk-foreground for the shimmer highlight. Both default to your app's own --muted / --foreground shadcn tokens when present, so it matches your theme automatically. Override either directly to use different colors:

<Skeleton className="[--sk-muted:theme(colors.zinc.200)] [--sk-foreground:theme(colors.zinc.600)]" />

Plain Tailwind background utilities (bg-accent, etc.) also work for the surface fill since className is merged last, but the shimmer highlight only follows --sk-foreground — override both if you want the two to stay in sync.

Transitioning into content

Wrap a placeholder and the real content in SkeletonTransition — when loading flips to false, the shimmer settles for a beat, then the skeleton and content crossfade:

import {
  SkeletonAvatar,
  SkeletonText,
  SkeletonTransition,
} from '@/components/sora-ui/effects/skeleton';

export default function Page({ loading, user }: { loading: boolean; user?: User }) {
  return (
    <SkeletonTransition
      loading={loading}
      skeleton={
        <div className="flex items-center gap-3">
          <SkeletonAvatar />
          <SkeletonText className="w-32" />
        </div>
      }
    >
      <div className="flex items-center gap-3">
        <img alt="" className="size-10 rounded-full" src={user?.avatarUrl} />
        <p>{user?.name}</p>
      </div>
    </SkeletonTransition>
  );
}

Size skeleton to match content

While both are mounted, children is absolutely positioned over skeleton, which drives the wrapper's height. If the two differ a lot in size, the layout snaps to the content's real height the instant the crossfade finishes. Keep skeleton close to the real content's dimensions to avoid a visible jump.

Props

Skeleton

PropTypeDefault
variant?
"shimmer" | "fade"
"shimmer"
duration?
number
1.6 (shimmer) / 2.4 (fade)
rounded?
"none" | "sm" | "md" | "lg" | "full"
"md"
animate?
boolean
true
decorative?
boolean
true
label?
string | null
-

SkeletonTransition

PropTypeDefault
loading?
boolean
-
skeleton?
ReactNode
-
children?
ReactNode
-
className?
string
-

When to Use

Use Skeleton for content that loads asynchronously and has a roughly known shape ahead of time — profile cards, list rows, article previews. It reduces perceived load time better than a spinner because it previews the coming layout. Reach for SkeletonTransition whenever the loading state and the real content live in the same spot, so the swap reads as one continuous moment instead of a layout jump.

Accessibility

By default, Skeleton is aria-hidden — a purely decorative placeholder alongside content that's coming. Set decorative={false} to announce it as a live loading status (role="status", aria-live="polite") when there's no other loading indicator on the page. When prefers-reduced-motion: reduce is set, shimmer animation stops (injected CSS is disabled), fade uses motion-reduce:animate-none, and SkeletonTransition skips its settle delay and crossfades immediately.

Built by Axyl. A motion-first component registry for React.

Last updated: 7/3/2026