In a fitness-tracker app, the workout-history screen's content jumps when real data replaces its skeleton, and users tap the wrong workout — why, and how do you fix it?
answer
- placeholder size versus real size
- late media with no reserved box
- content inserted above the reader
- build skeleton from the real layout
- reserve slots for late arrivals
basics
~20 sThe placeholder and the real content occupy different space, so arriving data pushes rows under the user's finger. Fix it by building skeletons from the real layout, reserving space for late media, and never inserting content above what the user is reading.
solid answer
~50 sA skeleton exists to hold the space real content will take; if it holds different space, the arrival of data **shifts the layout**, and a tap aimed at one workout lands on another. The usual causes are a skeleton row shorter than the real row, **route-map thumbnails** that load after the text without a reserved box, a late banner such as a sync-status or personal-best card **inserted above** the list, and text that wraps differently than the placeholder assumed. The fixes follow: build the skeleton from the same layout template and spacing values as the real row, give every image a fixed size or aspect before it loads, reserve slots for late elements or place them where they displace nothing, and keep the user's scroll position anchored to what they were reading. Then verify on a slow connection, because fast test networks hide the shift.
go deeper
Recall that a placeholder should take the same space as the content that replaces it, so nothing moves when data arrives.
Explain the common causes: undersized skeleton rows, media without reserved space, late elements inserted above, and text wrapping more than assumed.
Diagnose with throttled connections, frame-by-frame recordings and large text sizes, and fix structurally with skeletons derived from the real layout.
Push the fix into the system: components that ship matching skeletons and reserved media boxes, plus a rule that late content never displaces the reader.
## What layout shift is and why it hurts **Layout shift** is the unexpected movement of visible content after it has been drawn, caused by something arriving or resizing above or around it. It is a loading problem because most shifts happen at the moment placeholder content is replaced by real content. The harm is concrete: - **Mis-taps.** The user aims at 'Tuesday run', a row above it grows, and the tap lands on 'Monday ride' — or on a delete control. - **Lost reading position.** Text the user was reading jumps away. - **Disorientation** for users with low vision or cognitive disabilities, who rely on things staying where they were. - **Wasted skeletons.** A skeleton that causes a jump has failed at its only job. ## Diagnosing the workout-history screen In the scenario, the fitness app's history screen shows a list skeleton, then real rows arrive and everything jumps. The common causes, in rough order of likelihood: | Cause | What happens | Fix | |---|---|---| | Skeleton row shorter than the real row | Every arriving row pushes the next down | Build the skeleton from the real row's layout and spacing values | | Route-map thumbnail with no reserved box | Text renders, then the image pops in and grows the row | Give the media slot a fixed size or aspect ratio before it loads | | Late element inserted above the list | A sync-status bar or a 'personal best' card appears at the top and pushes the whole list down | Reserve its slot from the start, overlay it, or place it below the reading position | | Text wraps to more lines than the placeholder assumed | Long workout names or larger text settings grow the row | Size placeholders for realistic content and the user's text size | | Late-loading typeface with different metrics | Every text line changes height at once | Choose fallback metrics close to the final typeface, or reserve line height | | Skeleton count differs from real count | Three skeleton rows become twelve real rows, or one | Acceptable below the fold; above it, match the likely count | ## The design rules that prevent it 1. **One source of layout.** The skeleton for a list row should be derived from the same layout template and spacing tokens as the real row, so a change to the row updates the placeholder too. Hand-drawn skeletons drift. 2. **Reserve space for anything that loads later.** Images, charts, maps and ads need a known box before their content arrives. 3. **Never insert above the reading position without the user asking.** New items, banners and summaries go in reserved slots, below the viewport, or behind an explicit 'Show 3 new workouts' control. 4. **Anchor scroll to content.** If something must be inserted above, keep the item the user was looking at in place rather than keeping the pixel offset. 5. **Transition in place.** Replace each skeleton block with its real content at the same position; a cross-fade is fine, a resize is not. ## Verifying the fix - **Throttle the connection** in testing; on a fast office network, content arrives before anyone can see it move. - **Record the screen** during load and step through frames; shifts invisible at full speed are obvious frame by frame. - **Test at the largest text-size setting**, where wrapping differences are biggest. - On the web, the platform reports a layout-shift measure that can be tracked in field data; native platforms need screen recordings or UI tests that assert element positions before and after load. ## Why this belongs in the design system The fix is mostly **structural**: a list-row component that ships its own matching skeleton, a media component that always reserves its box, and a guideline that late content never displaces what the user is reading. Once those exist in the system, every product team gets shift-free loading by default instead of rediscovering the bug screen by screen.
- The team wants a 'New personal best' card to appear at the top of the history screen when it is computed a second after load. How do you place it without shifting?Either reserve its slot from the first render — showing a placeholder that may collapse only if nothing arrives, ideally before the user can interact — or place it where it displaces nothing: below the fold, as an overlay the user dismisses, or behind a control. What it must not do is push the list down under a user who has already started reading or tapping.
- Is a mismatched skeleton still better than no skeleton at all?Not necessarily. A blank area with a single spinner, followed by content appearing once, can be less disruptive than a skeleton that visibly reshapes itself. The skeleton's value comes from occupying the right space; if it cannot, fix it or use a simpler indicator in a fixed-size container.
saying these in an interview costs you the question
- A generic grey block is good enough for every skeleton
- Animating the insertion makes a layout shift acceptable
- Images can size themselves once they have loaded
- If it looks fine on the office network, there is no shift
- Loading the top banner faster eliminates the problem