A placeholder that holds the space content will occupy while it loads.
import { Skeleton } from '@oztix/roadie-components/skeleton'
One text line, as tall as the line height it inherits.
<Skeleton />
text is a line of copy, block is a panel such as an image or a card body, and circle is an avatar. Width and height come from Tailwind utilities, so the shape only sets the radius and a starting size.
<div className='grid gap-4'> <Skeleton shape='text' /> <Skeleton shape='block' /> <Skeleton shape='circle' /> </div>
Size a Skeleton the way you would size the content it stands in for. A text line tracks the type size around it, so put it in an element that carries the type class. Skeleton renders a div, so that element cannot be the p it stands in for.
subtle is the default. Use subtler on a raised or sunken surface where the default fill reads too loud.
<div className='grid gap-4'> <Skeleton emphasis='subtle' /> <Skeleton emphasis='subtler' /> </div>
Skeleton takes its intent from context, so a placeholder inside a tinted region picks up that palette. Pass intent to set it directly.
A paragraph is several text lines in a grid, with a short last line.
<div className='grid gap-2'> <Skeleton /> <Skeleton /> <Skeleton className='w-3/5' /> </div>
A list row is a circle beside a two-line stack.
Use a Skeleton once the page is on screen and its content is still arriving, such as an event list still fetching. For a full page that has not rendered yet, show nothing rather than a screen of grey bars.
Match the real layout. The skeleton should occupy the same box as the content that replaces it, so nothing jumps when the data lands.
Use a fixed number of rows, three or four, when the real count is unknown. A skeleton per expected record makes the wait look longer than it is.
Stop at one level of detail. A card skeleton is an image block, a title line and a meta line, not every element the card will eventually hold.
Every skeleton on screen shares one sweep. The highlight is anchored to the viewport rather than to each element, so a one-line placeholder and a full-width block show the part of the same band that falls where they sit, and a row of them reads as one surface catching the light rather than as separate things blinking in turn.
Two consequences worth knowing. Skeletons inside a pane that scrolls on its own pass under the band as they move, which reads like a fixed light source in the room. And in a browser that does not honour a viewport-anchored background, each skeleton runs its own sweep instead: still a shimmer, just not a shared one.
The root carries aria-hidden='true', so a screen reader reads nothing from the placeholder itself. A row of announced bars would be noise, and the shapes carry no information.
The region that owns the fetch announces the wait. Put aria-busy='true' on it while the request is in flight, and add a small live region that carries the wording. aria-busy alone changes no announcement, so without the live region a screen reader user hears nothing until they navigate back to the section.
Keep the live region small and separate from the content it describes. A role='status' on the whole section would read the entire result back once it lands.
The motion runs on the animate-shimmer utility from the motion tokens. The highlight and the tint pulse under it share one period, --duration-sweep, and one pass moves the gradient exactly its own width, so the loop closes on itself with nothing to see at the join. The highlight is lighter than the surface it crosses in both themes, from the --sheen-shade and --sheen-highlight tokens. Under prefers-reduced-motion: reduce the highlight is removed and the global motion reset cuts the pulse to a single pass, so the placeholder holds a static tint and never flickers.