Skip to main content
First-pass proposal, not established policy. No audited Invoca screen backs this page — Titan’s design library was not available while writing it. It is built from general interaction-design practice and from what Skeleton, Spinner, and ProgressBar already establish.It also sits on top of a real, verified finding, not a hypothetical one: TITAN-GAP-31. Skeleton — the component this page leans on most — carries no theme overrides, no tokens, and no representation in the design library at all, despite being the most-used component in this area: 132 usages across 13 applications. This page proposes how to use Skeleton consistently. It does not, and cannot, resolve what Skeleton is — that gap stays open, and everything below is a usage layer built on top of an undecided foundation.

The problem

A page that has nothing to show yet has three honest ways to say so: promise a shape (“here’s roughly what’s coming”), promise nothing but activity (“something is happening”), or promise a measurable amount (“this much is done”). Using the wrong one either misleads the reader about what’s arriving or wastes the one advantage the right one had. Get the duration wrong and the failure is different but just as visible. A loading state that flashes for 150ms and vanishes reads as a glitch. One that never resolves reads as a hang, and the reader has no way to tell a slow network from a broken one. Titan already ships all three primitives — Skeleton, Spinner, ProgressBar — with no decision about which to reach for when, and evidence that the gap has a real cost: three separate applications have independently built what appears to be the same TabLoadingSkeleton, at 21 instances each. That is not three teams making different choices. It is three teams answering the same unanswered question on their own.

Decide first: what do you actually know?

Skeleton is not the default just because it is the most-used. It is correct only when the shape is known. Reaching for it when the shape is a guess trades a blank region for a confidently wrong one — see TITAN-SKL-02.

When this applies

  • Content whose eventual layout — line count, widths, roughly how many rows or fields — is known before it arrives.
  • A wait long enough to notice, and a region large enough that a blank space would read as broken rather than loading.

When it doesn’t

Structure

Behavior

Constraints

Content

Accessibility

A skeleton is decorative; the region it sits in is not. Hide skeleton shapes from assistive technology and put aria-busy="true" on the region that’s loading. Announce once, when the content resolves — not the fact that loading started, and not on every render while it’s still in progress. A screen-reader user given a running commentary of “loading” has been told less than one given a single, timed “done.” Reduced motion is currently unhandled at the component level. Skeleton’s pulse and wave animations do not respond to prefers-reduced-motion: reduce — this is part of TITAN-GAP-31, and the same gap TITAN-GAP-08 records at the motion-foundation level: seven durations exist and none has a reduced variant. Until the component handles this itself, pass animation={false} at every call site under that preference — TITAN-LOADING-07. A spinner or progress bar needs an explicit accessible name at the call site. Neither sets one automatically — pair a Spinner with aria-describedby on the region it describes and aria-busy on that region; do the same for a ProgressBar reporting a specific process’s state.

Variations

Anti-patterns

The flashing skeleton. Showing a placeholder for a wait that resolves in 150ms turns a fast response into a visible flicker. The fix is not a faster skeleton — it’s not showing one at all under the threshold in TITAN-LOADING-02. The eternal skeleton. Nothing about Skeleton times out on its own. Without a timeout in the calling code, a failed or hung request looks identical to “still loading,” and the reader waits far longer than they should before concluding otherwise. Two animations in one screen. One region pulsing while another waves reads as two systems loading independently, not one page. Titan ships both animations with no decision between them — pick one and stay consistent within a screen. Rebuilding the skeleton screen per application. The pattern this page exists to prevent has already happened three times over — see TITAN-GAP-31. A composed skeleton screen worth naming is worth sharing.
Last modified on September 2, 2026