Loading placeholder that preserves layout while content is being fetched. Two presets: Custom (free-form) and Text (tied to typography styles).
v3.1.3
Skeleton
3.1
3.1
--
3.1.4
Overview
-
1
Custom
-
2
Text
-
3
WhiteOverlay
-
4
Corner radius: full
1
2
3
4
States
Skeleton has a single visual state — loading. The style (OnWhite or WhiteOverlay) is chosen based on the background it sits on.
OnWhite — on white / light grey backgrounds
-
Background --skeleton-on-white — the base style for the BaseWhite background (Update 3.1).
-
Wave --skeleton-wave — the gradient overlay sweeping on --pattern-shimmer.
-
Custom Shape border-radius comes from the host; the
Corner radius=fullvariant is skeleton--radius-full. -
Text Bar Fixed height + border-radius per typography style (see Layout).
WhiteOverlay — on colored or dark backgrounds
-
Background --skeleton-white-overlay via skeleton--white-overlay — the second style Update 3.1 shipped.
-
Wave --skeleton-wave — the same token; its Night value is dimmer by design.
-
Use When The skeleton sits on a coloured, image or dark surface.
Anatomy
Every skeleton instance — Custom or Text — is built from the same three-layer structure.
.skeleton
.skeleton-animation · .skeleton-gradient-walker (staging)
-
1
.skeleton The root and the whole web mechanism: the token background,
overflow:hidden, and the wave — an::afteroverlay riding --pattern-shimmer. Width and height come from the host. -
2
.skeleton-animation / .skeleton-gradient-walker The Figma STAGING of the wave (Figma cannot loop it: three staged frames + the 150×80 gradient bar). Kept in markup for parity with the Figma blocks; the walker never renders — the ::after is the one animation.
Layout
Text Skeleton bars have fixed heights and border-radii that correspond to each typography style. The wrapper adds vertical padding so the bar aligns correctly within the line-height.
Promo Heading
Heading 1
Heading 2
Heading 3
Heading 4
Promo Body
Main Body
Compact Body
Main Link
Compact Link
Caption
-
Max 2 Lines Even if the real content has 3+ lines, the skeleton shows at most 2 bars.
-
Width Side margins within the text container are customizable — set width on the .skeleton element.
-
No Font Scaling The bar sizes are the contract's FIXED values — they do not change with accessibility font-size increases and are font-agnostic. Heading 1's 30 sits off the 4px grid, recorded as-is; every other height is an SP token.
Bar geometry — measured
-
One Row, Measured The Main Body bar as the worked example: the wrapper owns the line-height (var(--sp-s20)) as var(--sp-s2) + bar var(--sp-s16) + var(--sp-s2), radius var(--sp-s6). Every other style follows the same recipe with its own trio — the full table above.
Animation
One motion: the wave. It rides the pattern named after this component — nothing is declared locally.
LTR — the wave sweeps left → right
RTL — the wave sweeps right → left
-
The Wave var(--pattern-shimmer) — Patterns/Shimmer, the token motion-rules names "the Skeleton sweep": 1500 ms, standard curve, infinite; the gradient is
transparent →--skeleton-wave→ transparenton the ::after overlay, translateX −100% → 100%. -
RTL The same token played backwards — animation-direction: reverse on [dir="rtl"] .skeleton::after; direction is a separate property, the shorthand stays the token's.
-
Reduced Motion Zeroed at the token — --pattern-shimmer resolves to none in
motion.css; the background colour alone carries the loading state. No component override. -
Retired The pre-3.1.3 web build ran its own 1500 ms linear keyframes with a local reduced-motion block and an RTL keyframe that animated
left— all replaced by the token recipe.
Usage
-
1
A Known Shape, Loading Longer Than ~300 ms Hold the layout while text, images, squircles, cards or whole rows arrive — the interface must not shift when the content lands. Under ~300 ms show nothing; unknown shapes take the Loader instead.
-
2
Max 2 Bars Per Text Block However many lines the real text has, its skeleton is at most two bars — a skeleton sketches the shape, it does not typeset the paragraph.
- 3
Loading
✕ A bar per text line — the contract caps a text block at 2 bars, however long the real text
Accessibility
Skeleton is a decorative loading state: it announces that content is coming, once, without interrupting whatever the screen reader is already saying.
Loading
1
2
-
1
A Separate Status Node Announces, Once role="status" on its own always-mounted node, "Loading" as its text. Polite: the announcement waits for a gap instead of cutting into ongoing speech. A live region announces a change, not its first content, and an aria-label change is not a live update.aria-busy="true" on the region being loaded — never on the status node, which a busy region would silence, and never on the shapes.When the real content arrives, drop aria-busy, take the skeleton out of the DOM and clear the status text.
-
2
A shape is never a target pointer-events: none and user-select: none on
.skeletonand .skeleton-text.No tabindex on any skeleton element. A placeholder that takes focus offers a control that does not exist yet. -
Reduced motion On the web the wave stops entirely — var(--pattern-shimmer) resolves to none at the token; the background colour alone carries the loading state. Recorded gap: BOTH shipped apps ignore the OS reduced-motion setting and keep shimmering — flagged.
-
The shipped platform gaps Android sets no accessibility semantics at all — the shapes are neither hidden nor marked busy by the DS; the host must add both today.iOS hides the shapes only on the
isLoading-gatedhasDsSkeletonpath — the baredsSkeleton(...)modifier leaves them exposed to VoiceOver. Both flagged in the a11y spec. -
Low contrast, on purpose var(--skeleton-on-white) against var(--background-primary) sits far below the text floor, and so does var(--skeleton-white-overlay) on a colour. WCAG 1.4.11 places no contrast requirement on decorative elements; the purpose is carried by aria-label, not by the contrast.