Import
import { TableSkeleton } from "@timelycare/helix-ui"
Props
interface TableSkeletonProps extends React.ComponentProps<"div"> {
columns: number
rows?: number // default 5
label?: string // default "Loading" — pass an already-translated string
}
Usage
{isLoading ? (
<TableSkeleton columns={4} rows={5} label="Loading appointments" />
) : (
<Table>
<TableHeader>…</TableHeader>
<TableBody>…</TableBody>
</Table>
)}
It renders its own Table, so it is a drop-in replacement for the table while data loads. Do not wrap it in another one.
Structure
| Element | Tailwind |
|---|---|
| Root | role="status" aria-busy="true" aria-live="polite" |
| Label | sr-only |
| Row | TableRow |
| Cell | TableCell |
| Bar | h-4 w-full (via Skeleton) |
Why a component rather than a copy-paste pattern
The Skeleton page shows a loading row as an arrangement you assemble yourself. This is that arrangement, shipped, with two things the snippet leaves to each caller:
- The bars sit in real table cells, so their widths follow the table's own column layout instead of fixed pixel widths that drift from the data.
- The live-region wiring is built in. The container is a polite live region with a visually hidden label, so assistive tech announces that the table is loading rather than reading out a grid of empty cells.
Choosing a Loading State
| Situation | Use |
|---|---|
| Table fetching its first page | TableSkeleton |
| Non-tabular content loading | Skeleton, shaped to the content |
| Short action inside a control | Spinner |
| Determinate progress | Progress |
| Finished loading, no results | Empty |
Accessibility
- The root is
role="status"witharia-live="polite"andaria-busy="true", so the loading state is announced without interrupting. labelis visually hidden text, not atitle. Pass it already translated; the library does no i18n and the default is English.- Nothing inside is focusable, so keyboard order is unchanged while loading.
- Placeholder bars are decorative and exempt from contrast minimums.
Gotchas
| Problem | Solution |
|---|---|
| Layout jumps when data arrives | columns does not match the real table's column count |
| Announced as a table of empty cells | Something replaced the root; keep role="status" on the wrapper |
| Skeleton never goes away | Loading state is not resolving — a skeleton is not an empty state; use Empty when the result set is genuinely empty |
| Label reads in English in a localized app | Pass a translated label |
Related
- Related Components: Skeleton (the bars themselves), Table (what it stands in for), Spinner (inline loading), Empty (no results)
- Patterns: Component Match — when to use a skeleton vs a spinner vs an empty state