UIDex

Skeleton

هيكل التحميل

aria-busy="true"

also calledskeleton screen، skeleton loader، content placeholder، shimmer، ghost loader، سكيليتون، الهيكل التحميلي، شاشة الهيكل، العناصر النائبة، هيكل المحتوى، شيمر

A skeleton is a stripped-down grey stand-in for the interface, drawn in the same places and at the same sizes as the content still being fetched. Its job is to tell you the shape of the page before the data arrives, so the wait feels shorter and nothing jumps when it lands. The difference from a spinner is that a spinner says "wait" and says nothing about layout, while a skeleton says "this is what is coming, and this is where". A progress bar reports a known fraction of measurable work, and a skeleton has no idea how much is left. An empty state is a final answer meaning "there is nothing here", while a skeleton is a temporary promise that data is on the way. A skeleton that never resolves is worse than a plain error. The governing rule: if the skeleton does not match the shape and size of the real content, the layout jumps when the data lands, and you have created the problem you were trying to hide.

If you called it…

"the grey boxes before the content shows up""the shimmer that sweeps over the cards while loading""the fake layout that appears first""the ghost version of the page""those grey bars where the text will be"

Live specimen

Interact with the demo. Every part is real and numbered.

حدّ أدنى 600msجارٍ التحميل…
RTLاللمعة تمشي مع اتجاه القراءة، جرّب تبديل اللغة من الأعلى

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a skeleton for a profile card. Wrap the loading region in an element carrying aria-busy="true" and drop the attribute the moment data arrives. Make every skeleton block aria-hidden="true" and give it the exact box of the real element it replaces: same height, same radius, same padding. Write padding and gaps as logical properties (padding-inline, margin-inline, gap) in both the skeleton and the real component, so the layout does not jump in RTL only. Render text lines as bars inside slots whose height equals the line-height, with the last bar at 55% width and no manual margins. Run one shimmer over the whole region instead of one animation per block, animate it with translateX(calc(100% * var(--dir))) with --dir: 1 on the root and -1 under [dir="rtl"], compute the gradient angle from that same variable, and disable the animation completely under prefers-reduced-motion: reduce. Add a single role="status" line for the whole region saying "Loading" then "Loaded", and never repeat it per block. Finally, delay showing the skeleton by 200ms, keep it on screen for at least 600ms once shown, and render a real error state rather than an eternal skeleton if the request fails.

How it behaves right-to-left

This section is ours alone.

Path of the sweep

mirrors

The sweep travels with the reading direction: left-to-right in English, right-to-left in Arabic. transform is a physical property and never flips with dir, so a hard-coded translateX(-100%) runs the shimmer backwards on an Arabic page. Drive it from a variable instead: --dir: 1 on the root, --dir: -1 under [dir="rtl"], then animate translateX(calc(100% * var(--dir))). For a plain linear sweep, animation-direction: reverse under [dir="rtl"] does the same job in one line.

Tilt of the gradient

mirrors

A linear-gradient angle is physical too: CSS still has no logical keyword such as "to inline-end" for gradients. A 110deg shimmer leans the wrong way once the page flips, so compute it from the same direction variable: linear-gradient(calc(90deg + 20deg * var(--dir)), …). The same applies to background-position if you animate the background instead of the element.

The geometry of the blocks

never mirrors

Do not fix the sweep direction by flipping the whole skeleton with transform: scaleX(-1). The blocks are not directional: mirroring them inverts asymmetric border-radius corners, reverses any placeholder icon inside, and flips the direction of any clip-path or mask-image on them. Keep the flip inside the motion (the --dir value in the keyframes) and leave the boxes alone. If you need asymmetric corners, write them as border-start-start-radius and border-start-end-radius so they mirror on their own.

The short last line

mirrors

The last bar in a paragraph starts at the logical start and falls short at the logical end: in Arabic it starts on the right and stops before the left edge. A block box with width: 55% and zero margins does this by itself, but margin-right: auto pins it to the wrong side, so use margin-inline-end: auto. A fade added with mask-image: linear-gradient(to right, …) is physical and needs an explicit flip under RTL.

Skeleton padding must equal component padding

mirrors

The skeleton and the real component must share the exact same padding and gap values. Write padding-inline-start on the real card and padding-left on its skeleton, and the two agree in English but diverge in Arabic, so the layout jumps on data arrival in Arabic only. That is the worst kind of bug, because it never surfaces in your own testing.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIAaria-busy="true"Marks the region busy so assistive tech defers announcing what changes inside it.
ARIArole="status" + aria-hidden="true"One announcement for the whole region, with every skeleton block hidden from the accessibility tree.
Material UI<Skeleton variant="circular" animation="wave" />variant picks the shape; animation="wave" is the shimmer and false turns it off entirely.
Ant Design<Skeleton active paragraph={{ rows: 3 }} />active enables the shimmer; paragraph.rows sets how many text-line bars are drawn.
shadcn/ui<Skeleton className="h-4 w-[220px]" />Just a div with a background and a pulse; matching the size is on you, and that is where it usually goes wrong.
SwiftUI.redacted(reason: .placeholder)The Apple equivalent: it redacts the real content in place, so size matching comes for free.
CSS@media (prefers-reduced-motion: reduce)The query that has to remove the shimmer completely rather than slow it down.

See also

Updated · 2026-08-19