Skeleton
Placeholders that hold the shape of content while it loads, so the page doesn't jump when it arrives.
Installation
npx @smicolon/fasla-ui@latest add skeletonRun 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.
defaultcircularrectangularAccessibility
- Every block is
aria-hidden, so screen readers skip the placeholders. - Mark the region that is loading with
aria-busy="true", and set it tofalsewhen the content arrives. - The pulse stops when the user turns on
prefers-reduced-motion. Setanimatetofalseto turn it off everywhere. - Direction is inherited from
dir, never a prop. The avatar always sits at the start of the line.
Comments
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "circular" | "rectangular" | "default" | The shape of a single block: a rounded bar, a circle or a rectangle. |
animate | boolean | true | Pulses while loading. The pulse always stops under reduced motion. |
lines | number | 2 | Number of text lines in SkeletonText, SkeletonListItem and SkeletonCard. |
className | string | — | Sets the size, such as h-4 w-48. |
Usage
A single block
import { Skeleton } from "@/components/ui/skeleton"
export function TitleSkeleton() {
return <Skeleton className="h-4 w-48" />
}Text lines
import { SkeletonText } from "@/components/ui/skeleton"
export function ParagraphSkeleton() {
return <SkeletonText lines={3} />
}A list row
import { SkeletonListItem } from "@/components/ui/skeleton"
export function RowSkeleton() {
return <SkeletonListItem />
}A card
import { SkeletonCard } from "@/components/ui/skeleton"
export function CardSkeleton() {
return <SkeletonCard />
}Mark the region that is loading
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>
)
}