affectUI › Components › Feedback
Skeleton
Gray shapes in the exact size of the content they stand in for – a soft sheen sweeping across all of them in step, a Loading wrapper that tells screen readers and fades in late, so fast loads never flash.
Free and open source (MIT). Install: npx shadcn@latest add https://www.affectui.com/r/skeleton.json · Import from @/components/ui/skeleton
When to use
- A list, table, card or panel waiting for its data – the page keeps its shape, nothing jumps when the data lands
- Text, avatars, badges and buttons whose size you know: give each skeleton the size of what it replaces
- Wrap the skeletons of one area in Loading – one status for screen readers, and nothing flashes when the data is quick
When not to use
- A single action that runs – use the Spinner, in the button or the row
- An agent thinking or writing an answer – use the Thinking Loader
- Data that never came – show an empty state with a first step, not skeletons forever
Usage
import { Loading, Skeleton } from "@/components/ui/skeleton";
export function RunList({ runs }: { runs?: Run[] }) {
// Loading gets the list's own class, so its rows sit in the same layout
if (!runs)
return (
<Loading label="Loading runs" className="run-list">
{[212, 164, 176].map((w, i) => (
<div key={i} className="run-row">
<Skeleton width={16} height={16} radius={4} />
{/* line: as tall as the 20px text line, the 10px bar centered in it */}
<span className="run-row__title">
<Skeleton line width={w} height={10} />
</span>
</div>
))}
</Loading>
);
return (
<div className="run-list">
{runs.map((r) => (
<div key={r.id} className="run-row">
<AgentMark id={r.agent} />
<span className="run-row__title">{r.title}</span>
</div>
))}
</div>
);
}Props
| Name | Type | Default | Description |
|---|---|---|---|
Skeleton · width | number | string | "100%" | px or any CSS length; never wider than its parent |
Skeleton · height | number | string | 12 | The shape's own height |
Skeleton · radius | number | string | 6px (--aui-radius-sm) | Corner radius – "50%" for an avatar |
Skeleton · line | boolean | number | string | Puts the shape in a box as tall as the text line it replaces (true = 1lh, or a length) and centers it there – the row keeps its height when the words land | |
Skeleton · ...span props | ComponentProps<"span"> | className, style, ref – with line they go to the line box | |
Loading · label | string | "Loading" | What loads, for screen readers – "Loading runs" |
Loading · className | string | The real layout's class, so the grid or list is the same one the data fills | |
Loading · ...div props | ComponentProps<"div"> | style, ref, data-* – everything goes to the wrapper |
Accessibility
- Skeletons are aria-hidden – a screen reader hears nothing of the gray shapes
- Loading is role="status" with aria-busy and a label, so it says once what is loading
- Swap Loading for the real content when the data lands; the status goes with it
Responsive
- Never wider than its parent (max-width: 100%), so fixed widths hold on phones
- Give the Loading wrapper the real layout's class – the narrow layout of the data applies to the skeletons too
- Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"
Requirements
- React 19
Related
- Spinner – A small turning arc for work that runs right now – a faint ring in the text color, any size, inside a button or a status row, announced only when it has a label
- Thinking Loader – A small working pill for AI replies – an eight-ray spinner and a shimmering label that rolls through phases and settles on a check
- Data Table – Spreadsheet-style table: select cells and ranges, edit in place, copy and paste, undo and sort
- Progress Tracker – Onboarding checklist with an animated progress bar, ticks that draw themselves and auto play