Skeleton

Placeholders that hold the shape of content while it loads, so the page doesn't jump when it arrives.

Installation

bash
npx @smicolon/fasla-ui@latest add skeleton

Run init once per project before adding components. See Installation.

List Item Skeleton

SkeletonListItem puts an avatar at the start of the line and text lines beside it, 16px apart. Use it for lists, comments and profile rows.

Card Skeleton

SkeletonCard is a 122px media block above 2 text lines. It has no border or background of its own, so place it inside a Card when the real content has one.

Text Skeleton

SkeletonText draws full-width lines, 16px tall and 8px apart. It shows 2 lines unless you set lines.

Avatar Skeleton

SkeletonAvatar is a 48px circle. Pass another size in className to match a smaller or larger avatar.

Build your own

For layouts the three ready-made skeletons don't cover, compose your own from single blocks.

A single block, sized with className. It replaces shadcn's Skeleton with no other changes.

variant sets the shape of a block: a bar with a 4px radius, a circle, or square corners.

default
circular
rectangular

Accessibility

  • Every block is aria-hidden, so screen readers skip the placeholders.
  • Mark the region that is loading with aria-busy="true", and set it to false when the content arrives.
  • The pulse stops when the user turns on prefers-reduced-motion. Set animate to false to turn it off everywhere.
  • Direction is inherited from dir, never a prop. The avatar always sits at the start of the line.

Comments

Props

PropTypeDefaultDescription
variant"default" | "circular" | "rectangular""default"The shape of a single block: a rounded bar, a circle or a rectangle.
animatebooleantruePulses while loading. The pulse always stops under reduced motion.
linesnumber2Number of text lines in SkeletonText, SkeletonListItem and SkeletonCard.
classNamestring—Sets the size, such as h-4 w-48.

Usage

A single block

tsx
import { Skeleton } from "@/components/ui/skeleton"

export function TitleSkeleton() {
  return <Skeleton className="h-4 w-48" />
}

Text lines

tsx
import { SkeletonText } from "@/components/ui/skeleton"

export function ParagraphSkeleton() {
  return <SkeletonText lines={3} />
}

A list row

tsx
import { SkeletonListItem } from "@/components/ui/skeleton"

export function RowSkeleton() {
  return <SkeletonListItem />
}

A card

tsx
import { SkeletonCard } from "@/components/ui/skeleton"

export function CardSkeleton() {
  return <SkeletonCard />
}

Mark the region that is loading

tsx
import { SkeletonListItem } from "@/components/ui/skeleton"

interface Comment {
  id: number
  text: string
}

export function Comments({ comments }: { comments?: Comment[] }) {
  const isLoading = comments === undefined

  return (
    <section aria-busy={isLoading} aria-labelledby="comments">
      <h3 id="comments">Comments</h3>
      {isLoading ? (
        <SkeletonListItem />
      ) : (
        <ul>
          {comments.map((comment) => (
            <li key={comment.id}>{comment.text}</li>
          ))}
        </ul>
      )}
    </section>
  )
}