Treat the skeleton as a layout contract

A skeleton is a temporary version of a component’s structure. It helps people understand where content will appear, but it can also make the interface feel unstable if the finished component has different dimensions. The goal is not to draw convincing fake text. It is to reserve approximately the same space and preserve the same relationships between elements before and after loading.

Start with the component that will receive the data. Keep its outer container, padding, grid columns, and major media area in place during loading. Change the contents of those regions rather than swapping an unrelated placeholder for the entire component. This matters especially in a list: if every loading row is much shorter than a loaded row, the rows below it will move as results arrive.

A skeleton cannot predict every title length or optional field. Design for the range your component supports, and decide what happens when real content exceeds it. A title might wrap to two lines, while a description might be limited to three. Those should be component decisions that apply to the loaded state, not restrictions added only to make the skeleton match.

Reserve space using the same geometry as the loaded card

Consider a card with a thumbnail, title, short description, and action. In a modern browser that supports CSS grid and aspect-ratio, place the skeleton and loaded content in the same card container. Give the thumbnail region an aspect-ratio of 16 / 9 in both states. Use the same padding, column gap, and breakpoint for both versions. An image that arrives later can then fill its reserved region rather than pushing the title downward.

For text, size placeholders in line units rather than arbitrary pixel heights. If the loaded title uses a particular font size and line-height, make its skeleton region about two of those line heights when two lines are expected. Give the description a separate region based on its own line-height. Several bars of different widths can suggest lines of text, but their containing region should determine the space reserved. A short final bar is a visual detail, not a layout measurement.

Avoid fixing the entire card to one height unless the loaded design also has a clear overflow rule. At narrow widths, text may wrap more than it does on a desktop. Shared responsive rules and a sensible minimum height are usually safer than a large fixed height that clips content. If an action may be absent, consider whether its region should remain reserved in all cards or whether that card variant needs a different skeleton.

Make the loading state honest

A placeholder should resemble the kind of content that is coming, without suggesting details that are not known. Use a rectangular region for an image and simple bars for text. Do not show a skeleton button that looks ready to press when no action is available. If the loaded component can contain an error or empty state instead of a card, plan those outcomes separately rather than leaving the skeleton visible indefinitely.

Subtle animation can help distinguish a skeleton from finished content, but motion is optional. A static fill works well. If you add a pulse or shimmer, keep it restrained and disable it when the user prefers reduced motion. Do not use an animation as a progress indicator: a repeating shimmer does not tell anyone how much work remains.

Avoid replacing content that is already usable just because a background refresh has started. Keeping the existing card in place, with a modest updating status when needed, often prevents movement and preserves context. Skeletons are most useful when the content area has no meaningful content to display yet.

Keep placeholder details out of the accessibility tree

Individual skeleton bars do not convey useful information to assistive technology. Mark the decorative placeholder group as hidden from the accessibility tree, and give the surrounding content region an appropriate loading state, such as aria-busy while its contents are being updated. If the wait needs an announcement, provide one concise status message such as “Loading cards.” Avoid announcing every placeholder or repeatedly announcing an ongoing animation.

When the result arrives, replace the placeholders with the real content and clear the busy state. When loading fails, show an understandable error with an available next step instead of retaining the loading appearance. Keep focus stable throughout: loading should not move keyboard focus into a decorative card or unexpectedly send it elsewhere when the card appears.

Test the swap, not just the screenshot

A useful local test needs only a browser and a component with a delayed data response. Render several skeleton cards, wait briefly, then replace them with sample cards containing a short title, a long wrapping title, an image, and an optional field. Repeat the test at narrow and wide viewport sizes. Watch a point below the list: if it jumps when cards appear, compare the loaded and loading dimensions to find which region changed.

Also test with images loading after text, a font change, reduced-motion enabled, and a failed request. Image dimensions should already be reserved; a late image should not resize its card. Check that long content remains readable rather than being clipped to satisfy a placeholder height. Finally, navigate with a keyboard and a screen reader if available to confirm that the loading message is understandable and the decorative bars are not announced.

Refine the shared component layout before fine-tuning the bars. A skeleton that looks less like the final text but holds the right space is more useful than a detailed imitation that moves the page when real content arrives.

Back to category ↗