Updated
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.
Updated
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.
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 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 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.
| Appetite UI component | iOS counterpart | Material 3 counterpart | What differs |
|---|---|---|---|
| Skeleton, Line 200 x 12, Circle 40, Block 200 x 120 | SwiftUI redacted(reason: .placeholder), no Figma component | No counterpart, progress indicators only | Appetite 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.
| Property | Type | Default |
|---|---|---|
Type | Block, Circle, Line | Block |
Pulse | 1, 2 | 1 |
| Component | Variants | Size |
|---|---|---|
Skeleton | 6, Type crossed with Pulse | 200 x 12, 40 x 40, 200 x 120 |
_Skeleton List Row | One component, no properties | 360 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.
| Token | Light | Dark | Applied to |
|---|---|---|---|
bg/sub-200 | #D1D1D6 | #636366 | Pulse 1, the stronger end of the breath |
bg/soft-100 | #E9E9EA | #48484A | Pulse 2, the weaker end |
| Motion token | Resolves to | Value |
|---|---|---|
motion/skeleton/pulse/duration | motion/duration/slower | 480ms |
motion/skeleton/pulse/easing | motion/easing/linear | cubic-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.