Installation
npx gbs-add-block@latest -a Skeleton -betaThe block copies the skeleton folder into your project, along with the small shared folder that every component imports. You own the code and can change it freely.
There are no peer dependencies other than React.
Requirements
- React 19 and
@types/react19 - TypeScript target ES2022 or newer, with
"jsx": "react-jsx" - Browsers from 2024 or newer (the styles use
light-dark()andcolor-mix())
Import the stylesheet once, for example in your global CSS:
@import "../components/skeleton/styles.css";or in your root layout / entry file:
import "@/components/skeleton/styles.css";Two states that every list and panel eventually needs: one while the data is coming, and one for when it arrives and there is nothing in it.
Default
Quick Start
import { Skeleton, Empty } from "@/components/skeleton";
import "@/components/skeleton/styles.css";
{loading ? (
<Skeleton lines={3} label="Loading activity" />
) : items.length === 0 ? (
<Empty
title="No activity yet"
description="Once someone uploads a file, it will show up here."
actions={<Button onClick={invite}>Invite your team</Button>}
/>
) : (
<ActivityList items={items} />
)}Props Table
Skeleton
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "text" | "circle" | "rect" | text | The shape to draw. |
lines | number | 1 | Bars, for text. |
lastLineWidth | number | 60 | Width of the last line, as a percentage. |
width, height | number | string | — | A number is pixels; a string is used as given. |
radius | number | string | — | Corner radius for rect. |
animation | "pulse" | "wave" | "none" | pulse | How it moves. |
label | string | — | Announced while the placeholder is up. |
className, classNames, style | — | — | Slots: root, line. |
ref | Ref<HTMLDivElement> | — | The root element. |
Empty
| Prop | Type | Description |
|---|---|---|
title | ReactNode | What is missing, in a few words. |
description | ReactNode | Why, and what to do about it. |
icon | ReactNode | Decorative; hidden from assistive technology. |
actions | ReactNode | The way out — usually one button. |
size | "sm" | "md" | "lg" | Default md. sm suits a panel inside a page. |
className, classNames, style | — | Slots: root, icon, title, description, actions. |
Match the shape, not the pixel
A skeleton earns its keep when it takes the same space as what replaces it, so the page does not jump when the data lands:
<div style={{ display: "flex", gap: 12 }}>
<Skeleton variant="circle" width={40} />
<div style={{ flex: 1 }}>
<Skeleton lines={2} />
</div>
</div>With more than one line the last is drawn short, because real text does not end flush with the margin. A single line stays full width: one short bar reads as a label rather than a sentence still loading.
What screen readers hear
The bars are hidden from assistive technology. There is nothing useful to announce about the shape of a grey box, and a dozen of them announcing themselves is worse than silence.
Say the wait once, in whichever place fits:
{/* Either here… */}
<Skeleton lines={3} label="Loading activity" />
{/* …or on the region, if it already says it is busy. */}
<section aria-busy={loading}>
<Skeleton lines={3} />
</section>Empty is a normal region, not a live one: an empty result is part of the page rather than an alert, and a search that finds nothing should not interrupt what someone is reading.
Motion
pulse fades, wave runs a sheen across the bar, none is static. All of them stop under prefers-reduced-motion — an animation that repeats forever is exactly what that setting is about.
Styling and Theming
All rules are in the CSS components layer, so utility classes passed through className / classNames override them.
CSS variables
| Variable | Used for |
|---|---|
--sk-base | The bar itself. Reads --gbs-skeleton, then --gbs-hover. |
--sk-sheen | The highlight in the wave animation. |
--sk-radius | Corner radius of rect and circle. |
--sk-duration | One cycle of the animation (default 1.4s). |
--sk-width, --sk-height | Set by the width and height props. |
--sk-fg, --sk-muted | Empty's title and description. |
Override them on .sk-root / .em-root, on :root, or through style. Each variable falls back to the shared --gbs-* of the same name, then to the DataGrid's --dg-* when that stylesheet is loaded, and finally to the built-in palette.
Data attributes
| Element | Attributes |
|---|---|
Skeleton (.sk-root) | data-variant, data-animation, data-lines |
Empty (.em-root) | data-size |
Headless Use
core/lines.ts is framework-free:
import { lineWidths, toLength } from "@/components/skeleton";
lineWidths(3); // ["100%", "100%", "60%"]
lineWidths(1); // ["100%"] — one line is never shortened
toLength(24); // "24px"
toLength("3rem"); // "3rem"Next.js
Both are marked "use client" because they accept refs and style props, but neither holds state — render them from wherever your loading check lives.
Notes
- Nothing animates when the tab is in the background; the browser pauses CSS animations there on its own.
Emptycentres its content and caps the description at 46 characters per line, which is about where a paragraph stops being scannable.