Foundations
Loading & Skeletons
Instagram uses a shimmer skeleton pattern for first-load states and spinners only for user-triggered actions. The shimmer sweep timing and skeleton shapes are defined per-surface to prevent layout shift when content resolves.
Loading state system
Five loading modes — one for each scenario
The right loading pattern depends on whether the content shape is known, whether the user triggered the wait, and whether the user has seen the content before.
Skeleton
Shimmer sweep (translateX + gradient)
First load or empty cache — the full page shape is unknown to the user
Feed, profile headers, story trays, explore grids
Never skeleton a page the user has already seen — use stale content + background refresh instead
Shimmer only (no shape)
Shimmer on a solid block
Content area dimensions are known but content isn't, e.g. a DM thread with known height
Message thread first load, Settings rows
Don't use for media — the aspect ratio matters too much for layout stability
Spinner
Rotating arc, --ig-primary-button colour, 1s linear
A user-triggered action with unknown completion time (submit, upload, refresh)
Pull-to-refresh, post submission, follow action on slow connection
Don't use spinner for initial page load — a skeleton is less disorienting
Progress bar
scaleX from 0 to 1, linear timing at the operation's own speed
An operation with known progress — typically file upload or Stories playback
Media upload, Story progress (see Stories Progress component)
Don't fake progress — if the operation duration is unknown, use a spinner instead
Stale content + background refresh
None — content stays visible, no loading state shown
The user has seen this content before and the cache is still valid
Feed revisit, profile revisit, returning to a conversation
Don't flash a skeleton when stale content is available — it creates a worse experience than showing old data
Shimmer anatomy
The shimmer sweep
The shimmer is a light-coloured gradient that sweeps left-to-right across a placeholder shape. It communicates 'content is loading' without suggesting a specific time-to-complete.
/* Base skeleton shape */
.skeleton {
background: rgb(var(--ig-highlight-background));
border-radius: var(--radius-sm);
position: relative;
overflow: hidden;
}
/* The shimmer overlay */
.skeleton::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(var(--ig-primary-background), 0.6) 50%,
transparent 100%
);
transform: translateX(-100%);
animation: shimmer 1.6s infinite;
}
@keyframes shimmer {
to { transform: translateX(100%); }
}
/* Circle variant */
.skeleton--circle { border-radius: var(--radius-pill); }/* Light: --ig-highlight-background = rgb(239,239,239), overlay = white-tinted
Dark: --ig-highlight-background = rgb(38,38,38), overlay = white-tinted
Both cases: the same ::after gradient works because it uses the
--ig-primary-background token, which is 255, 255, 255 (light) and 12, 16, 20 (dark) */
[data-theme="dark"] .skeleton::after {
background: linear-gradient(
90deg,
transparent 0%,
rgba(var(--ig-elevated-background), 0.5) 50%,
transparent 100%
);
}Skeleton patterns
Confirmed shapes per surface
Each skeleton pattern mirrors the exact layout of the resolved content. The slot dimensions are derived from the component specs documented in the Components section.
Feed post skeleton
Home feed, Profile grid
The media block is the dominant placeholder — match the known aspect ratio if available (4:5, 1:1, etc.) to avoid layout shift when the image loads.
Profile header skeleton
Profile page header
Follower/following/posts stats are always three equal-width blocks, centred. Reserve their exact space to prevent the header from reflowing when data arrives.
Story ring skeleton
Story tray, Profile highlights
Render 5 placeholder rings to match the typical visible count in the tray. The ring border (gradient when unread, grey when seen) should stay invisible during skeleton state.
Explore grid skeleton
Explore, Search results
Use a 3-column uniform grid with 2px gaps. Every cell is a square (1:1 ratio). Stagger the shimmer timing slightly between cells (20ms offset) so the sweep doesn't look synchronised.
Comment list skeleton
Comments sheet
Render 4–6 skeleton comment rows to fill the visible area. Don't show the input composer until comments load — it avoids a jarring layout shift.
Timing
Animation timing rules
The shimmer duration and stagger are critical to the perception of responsiveness — too fast looks broken, too slow feels like a bug.
Shimmer duration
1.6sFull sweep cycle. Slower than 2s feels broken; faster than 1.2s looks jittery.
Shimmer easing
linearThe gradient itself creates the ease effect — the animation timing function is always linear.
Stagger between rows
20–40msOffset each row's animation-delay slightly so rows don't sweep in a synchronised wave.
Spinner duration
1sOne full rotation per second. Instagram's spinner uses linear timing, not ease-in-out.
Spinner size
20–24pxMatches --icon-size-md or --icon-size-lg. Use 20px inline; 24px for full-screen waits.
Min visible time
300msShow the loading state for at least 300ms — an instant flash of skeleton followed by content is more jarring than no skeleton at all.
Do
- Match skeleton shapes to resolved content — A 4:5 image skeleton prevents layout shift. A generic grey rectangle that resizes when the image loads creates a jarring jump.
- Show stale content when available — If the user has seen this content before, show the cached version and refresh in the background. Skeletons are for first load.
- Use --ig-highlight-background for skeleton fills — This token is defined for both light (239,239,239) and dark (38,38,38) mode — the skeleton automatically adapts.
- Stagger row animation-delay — 20–40ms offset between rows prevents the synchronised wave that makes skeletons look low-effort.
Don’t
- Don't use skeleton for actions — When a user taps Follow or Post, show a spinner inline in the button — not a skeleton. Skeletons are for initial content load, not interactions.
- Don't fake progress — A progress bar that isn't based on real progress (upload %, download %) must be replaced with a spinner. Fake progress confuses users when it stalls.
- Don't skeleton known-height empty states — If the surface has legitimately no content, show the empty state copy immediately — don't skeleton an empty list.
- Don't animate in reduced-motion — The shimmer animation must be removed under prefers-reduced-motion. The static skeleton fill communicates loading without movement.
@media (prefers-reduced-motion: reduce) {
.skeleton::after {
animation: none;
/* Static skeleton still visible — just no sweep */
}
}