TableSkeleton

Loading placeholder shaped like the table it stands in for. Skeleton bars are laid out in real table rows and cells, so the columns line up with the data that is about to arrive and the page does not jump when it does.

Spec · from metadata

When to use

  • A table is fetching its first page of data and you want the layout to hold its shape
  • Any list or data view where a spinner would collapse the page and then shift it back
  • Dashboard tables that load independently of the rest of the page

When not to use

  • Non-tabular content — use `Skeleton` directly and shape it to the content
  • A short action that resolves in well under a second — use `Spinner` or nothing at all
  • An empty result after loading finishes — use `Empty`, not a skeleton
  • Refetching data that is already on screen — keep the current rows and use a subtler pending state

Anti-patterns

Avoid{isLoading && <Spinner />}
<Table>…</Table>
Prefer{isLoading ? <TableSkeleton columns={4} /> : <Table>…</Table>}

A spinner collapses the table's space and then the rows push the page back down when they arrive. The skeleton holds the shape, so nothing moves.

Avoid<div className="flex gap-4">
  <Skeleton className="h-4 w-[80px]" />
  <Skeleton className="h-4 w-[120px]" />
</div>
Prefer<TableSkeleton columns={2} rows={5} label="Loading students" />

Hand-rolling the arrangement means re-deriving the column widths and re-adding the screen-reader wiring at every call site, and the bars won't line up with the real columns.

Accessibility

  • Required ARIAaria-busyaria-live
  • Screen readerThe container is a polite live region carrying a visually hidden label, so assistive tech announces that the table is loading rather than reading a grid of empty cells. Pass `label` already translated.
  • ContrastPlaceholder bars are decorative and exempt from contrast minimums; they inherit Skeleton's bg-accent.

Token bindings

TokenCategoryUsage
accentcolorPlaceholder bar fill (via Skeleton)
rounded-mdradiusPlaceholder bar corners (via Skeleton)
Recent changes
  • new

    TableSkeleton compound

    2026-08-12

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

ElementTailwind
Rootrole="status" aria-busy="true" aria-live="polite"
Labelsr-only
RowTableRow
CellTableCell
Barh-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

SituationUse
Table fetching its first pageTableSkeleton
Non-tabular content loadingSkeleton, shaped to the content
Short action inside a controlSpinner
Determinate progressProgress
Finished loading, no resultsEmpty

Accessibility

  • The root is role="status" with aria-live="polite" and aria-busy="true", so the loading state is announced without interrupting.
  • label is visually hidden text, not a title. 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

ProblemSolution
Layout jumps when data arrivescolumns does not match the real table's column count
Announced as a table of empty cellsSomething replaced the root; keep role="status" on the wrapper
Skeleton never goes awayLoading 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 appPass 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