Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Table Loading Skeleton Block for Ark UI

A projects table whose loading state keeps every cell, row and column the same size: Reload swaps each cell for a Skeleton of its own size for 1.6 seconds, with aria-busy and a polite status line.

Framework
UI library

Preview

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Overview

Show a table waiting without moving anything, with this table loading state for React with Ark UI. It opens loaded: eight projects of a studio called Projectyard with client, budget, a progress bar and a deadline, in a native table with fixed column widths. Reload swaps every cell for a Skeleton drawn at that cell's own size for a fixed 1.6 seconds, then back to the same data. The placeholder and the data share one grid cell, so the row, the column and the panel keep their size in both phases and nothing jumps when the data returns. The region says aria-busy while it waits, a polite line reads "Loading projects…" and then "8 projects loaded", and the pulse is turned off under reduced motion. The Button keeps its place, its size and its focus while it spins. On a phone the Client column folds under the project's name. For any table that loads from a server, with the layout of the loaded state as the loading state.

FAQ

How does it avoid layout shift?

Each cell is a one-cell grid holding both the Skeleton and the real content, stacked. The cell is as big as the larger of the two, and switching state only changes which is visible (visibility, so the hidden one leaves the accessibility tree too). The columns use fixed widths and the rows a fixed height, so a long client name or a short one cannot change the table. Compare this with a spinner row above the table, which pushes the data down when it arrives.

What stays local, and how do I tie it to a real request?

The wait is a 1.6 second timer and the projects are typed here; nothing is fetched. In your app, derive the phase from your query: loading while the request is in flight, ready when it has data, and put your rows into the same cells. If you refetch in the background, keep showing the old data rather than a skeleton, and use the skeleton only for the first load or a changed query.

How do I size the placeholders for my own columns?

Give each Cell a bar class with the height and width of the real content: h-3.5 w-28 for a name at the text size, h-2 w-24 for a progress bar, a short block pushed to the end with ms-auto for a number. Use the longest ordinary value, not the longest possible one. Keep the row height set on the tr, and do the same for any second line, like the client under the project name on a phone.

What do screen readers hear while it loads?

The figure is marked aria-busy, the skeleton boxes are hidden from assistive technology, and the real cell contents are hidden too while it waits, so there is no half-loaded table to read. The one polite status line says "Loading projects…" and then "8 projects loaded", and the Reload button stays focused and says it is unavailable while it waits, instead of dropping focus. With reduced motion the skeleton does not pulse and the icon does not spin.

What happens with reduced motion?

The pulse on the Skeleton and the spin on the Reload icon are switched off, and the swap between placeholder and data has no fade. The wait itself is not shortened, because it stands for a real request: the placeholders show for the same 1.6 seconds, still, and the status line reports both phases.

How does it look on a phone?

The Client column goes and its value moves under the project's name, with its own placeholder line, so the row keeps the same two lines in both phases. The progress bar is shorter, the budget and deadline stay in their columns, and the table scrolls sideways inside its frame only if a very large text size needs it.