Skip to content

Skeleton

Skeleton holds the shape of content while it loads, so the screen does not jump when it arrives. For lists and cards it beats a spinner.

On this page
  1. Preview
  2. Shapes
  3. Pulse
  4. iOS and Material 3
  5. Properties
  6. Tokens
  7. Guidelines

Preview

A placeholder that holds the shape of what is coming, so the screen does not jump when the data lands. Stack three to five rows while the data loads.

_Skeleton List Row, repeated three times360 x 72, circle 40, lines at gap 8, pulse 480ms

One row is a 40 circle, a line that fills the rest and a short line under it, all breathing together. The pulse runs 480ms on a linear curve, the timing named motion/skeleton/pulse.

Which shapes does Skeleton have?

Three: Line, Circle and Block, and nothing else. No text, no icons, no content of any kind.

Line, Circle and Block200 x 12, 40 x 40, 200 x 120

Line and Circle are both radius full, so a 12 tall line is a stadium and a 40 square is a disc. Block is the only one on a real radius token, dimensions/radius/lg 12, so it reads as a card.

Every size here is a default meant to be resized on the instance.

How does Skeleton pulse?

Two variants make one breath: Pulse 1 is the stronger gray and Pulse 2 the weaker. Held still here so you can see both.

Pulse 1 then Pulse 2, at Line, Circle and Blockbg/sub-200 to bg/soft-100

Pulse is a timeline, not a state. Both ends move the same way in either theme: light runs #D1D1D6 down to #E9E9EA, dark #636366 down to #48484A, so it always breathes from more contrast to less.

In code, animate between the two colors and alternate. When the reader prefers reduced motion, show Pulse 1 without the animation.

How does Skeleton differ on iOS and Material 3?

Neither platform draws a skeleton in the Figma file. SwiftUI offers the redacted modifier with the placeholder reason, and Material 3 leaves loading to its progress indicators.

Skeleton and its platform counterparts, Appetite UI component, iOS, Material 3, what differs
Appetite UI componentiOS counterpartMaterial 3 counterpartWhat differs
Skeleton, Line 200 x 12, Circle 40, Block 200 x 120SwiftUI redacted(reason: .placeholder), no Figma componentNo counterpart, progress indicators onlyAppetite UI draws the shapes you place. SwiftUI blanks the real content in place. Neither platform defines a pulse

A skeleton has no interaction, so no touch target applies. It carries no text, so announce the loading state once on the container, for example with a label such as Loading, and keep the gray shapes out of the reading order.

The shapes sit on bg/sub-200, which is too faint to carry contrast, so they signal shape and nothing else. See Progress for a spinner.

Properties

Two axes, six variants, and one ready made row beside them.

Properties, property, type, default
PropertyTypeDefault
TypeBlock, Circle, LineBlock
Pulse1, 21
Properties, component, variants, size
ComponentVariantsSize
Skeleton6, Type crossed with Pulse200 x 12, 40 x 40, 200 x 120
_Skeleton List RowOne component, no properties360 x 72 at radius 4

Check the ready made row against the list it stands in for before you use it as is. It is 360 by 72 at radius 4 with a 16 gap, a circle at radius full and 272 of content, and none of that is bound to the List component.

A stack of these holds the shape of the list only if you resize the instance to match.

Tokens

Two grays and one piece of motion. That is the whole component.

Tokens, token, light, dark, applied to
TokenLightDarkApplied to
bg/sub-200#D1D1D6#636366Pulse 1, the stronger end of the breath
bg/soft-100#E9E9EA#48484APulse 2, the weaker end
Tokens, motion token, resolves to, value
Motion tokenResolves toValue
motion/skeleton/pulse/durationmotion/duration/slower480ms
motion/skeleton/pulse/easingmotion/easing/linearcubic-bezier(0, 0, 1, 1)

Linear is right here and unusual everywhere else in the system: a breath that eases reads as a thing being tapped. No semantic loading token exists: the two grays are the background tokens used for dividers and empty states, so a skeleton on bg/soft-100 loses one end of its pulse.

Guidelines

Do

Repeat the row and let it hold the shape of the list underneath. Three to five is the range to use, and nothing jumps when the data arrives.

Don't

Do not stand one gray block in for a list. It promises a shape the content will not have, so the layout jumps anyway and it says less than a spinner would.

Reach for it when the layout is known and the wait is short: lists, cards, a profile header. Match the skeleton to the real thing and resize the instance rather than accept the defaults.

Do not skeleton a whole screen, because a page of gray is harder to read than one spinner. Cap it: past about ten seconds, switch to an error state.