Skeleton
Grey shapes where content is about to be, so the page keeps its layout while it loads.
- Switch Loaded on: each placeholder had the size of the content inside it, so nothing moves.
- While loading, the placeholders are hidden from screen readers; the card says
aria-busyinstead. - A soft shimmer crosses each shape. With reduced motion it stops and the grey stays.
Field marks
- Import
import { Skeleton } from "cennge-ui";- Size
- 0.5 KB gzip for this import, its dependencies aside
- Pattern
- A
<div aria-hidden>; once loaded, only its children - State
isLoaded: off shows the placeholder, on shows the children- SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Sizing
- From your classes (
h-4 w-32), or from the children it wraps - Source
- src/components/skeleton
When to use#
Use a Skeleton when you know the shape of what is loading: a repository card, a list of releases, a profile. The reader sees where things will be, and nothing jumps when the data arrives. When the shape is unknown, or the wait is for an action rather than content, a spinner is more honest.
Similar#
- Spinner says that something is at work, without promising a shape.
- Progress shows how much of a task is done.
- EmptyState is what to show once loading is over and there is nothing.
Shapes made with classes#
With no content to wrap yet, size each placeholder with classes: h-4 w-2/5 for a line, size-10 rounded-full for an avatar. Mark the list aria-busy while it loads.
import { Skeleton } from "cennge-ui";
export default function ReleaseListSkeleton() {
return (
<ul aria-busy="true" className="flex w-full max-w-md flex-col gap-5">
{[0, 1, 2].map((row) => (
<li key={row} className="flex items-center gap-3">
<Skeleton className="size-10 shrink-0 rounded-full bg-default-bg" />
<div className="flex flex-1 flex-col gap-2">
<Skeleton className="h-4 w-2/5 bg-default-bg" />
<Skeleton className="h-3 w-4/5 bg-default-bg" />
</div>
<Skeleton className="h-8 w-20 rounded-medium bg-default-bg" />
</li>
))}
</ul>
);
}Wrapping the real content#
Put the content inside and flip isLoaded. While loading, the children are rendered but invisible, so the placeholder takes their exact size; once loaded, the Skeleton renders the children alone. w-fit keeps a line's placeholder as wide as its text.
Latest release
cennge-ui 0.3.1
import { useEffect, useState } from "react";
import { Button, Skeleton } from "cennge-ui";
export default function LatestRelease() {
const [isLoaded, setLoaded] = useState(true);
useEffect(() => {
if (isLoaded) return;
// Stands in for the request.
const timer = setTimeout(() => setLoaded(true), 1500);
return () => clearTimeout(timer);
}, [isLoaded]);
return (
<div className="flex w-full max-w-md flex-col gap-4">
<div aria-busy={!isLoaded} className="flex flex-col gap-1.5 rounded-large bg-surface-3 p-4">
<Skeleton isLoaded={isLoaded} className="w-fit bg-default-bg">
<p className="text-sm text-muted">Latest release</p>
</Skeleton>
<Skeleton isLoaded={isLoaded} className="w-fit bg-default-bg">
<p className="text-2xl font-semibold">cennge-ui 0.3.1</p>
</Skeleton>
</div>
<Button variant="flat" isDisabled={!isLoaded} onClick={() => setLoaded(false)} className="self-start">
Reload
</Button>
</div>
);
}Wrap block elements, as here. Around an inline element, such as a Badge on its own, the placeholder takes the height of a line of text and can be a few pixels taller than the element.
Styling#
The placeholder is filled with surface-3, the same surface as a Card, and in the light theme that is lighter than the panels around it. Inside a card, or on a light panel, give it a tint that shows on any surface: the examples on this page pass bg-default-bg.
Props#
Also takes every attribute of <div>.