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.
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.
Match the indicator to what’s known: a real value → ProgressBar; a known content shape → Skeleton; neither → Spinner.
Each primitive communicates a different amount of information. Using the wrong one either overstates what’s known or wastes information that was available.
TITAN-LOADING-02
Show no loading indicator at all for a wait expected to resolve in under roughly 300ms.
Below that, the indicator itself becomes the most noticeable thing on screen — it appears and disappears faster than the reader can register what it was, which reads as a flicker rather than feedback.
TITAN-LOADING-03
A skeleton screen matches the real layout — same line count, widths, and spacing as the content it replaces.
Its only value is preparing the reader for a shape. A mismatch makes the page jump on load, which undoes the reason to use one at all.
TITAN-LOADING-04
One animation, one loading primitive, per region. Do not mix Skeleton’s pulse with wave, or a skeleton with a spinner, in the same view.
Two loading rhythms in one region read as two different systems, not one thing loading.
TITAN-LOADING-05
A loading state always resolves — to content, an EmptyState, or a handoff to Error handling. It never simply stops appearing.
An indefinite loading state is a hang the interface is disguising as progress.
TITAN-LOADING-06
A skeleton screen used on more than one page is built once and shared, not re-implemented per application.
Three independent copies of the same TabLoadingSkeleton, 21 instances each, are the measured cost of not doing this — see Known issues.
TITAN-LOADING-07
Under prefers-reduced-motion: reduce, pass animation={false} to every Skeleton on the screen.
The component does not honor the preference itself (see Known issues). This is a call-site workaround, not a fix.
TITAN-LOADING-08
The loading region carries aria-busy and announces completion once; the placeholder itself is hidden from assistive technology.
Grey shapes carry no information to a screen reader. The announcement belongs to the region, and only on completion.
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 — the same absence recorded at the
motion-foundation level generally, where seven durations exist and none has a reduced variant
(see Known issues). Until the component handles this itself, pass
animation={false} at every call site under that preference.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.
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 ~300ms threshold.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 Known issues. A composed skeleton
screen worth naming is worth sharing.
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). Three applications have independently built the same TabLoadingSkeleton.
Reduced motion is unimplemented at the token layer — seven durations exist and none has a reduced variant, which is why TITAN-LOADING-07 is a call-site workaround rather than a fix.
Where the exact line sits between “well under a second” (show nothing) and “noticeable” (show
an indicator) — this page names the shape of the threshold without a specific number beyond
the ~300ms already stated in Constraints.
What a ProgressBar should do when the underlying measurement stalls — hold its last known
value, or show some “no progress” signal distinct from a hang.
Whether a skeleton screen, once built once and shared, should live as a Titan export or stay
an application-level composition — the constraint says “shared,” not where.
The decision runs on what’s actually known, not on which primitive looks best, because each of
the three makes a different claim and a wrong claim costs the reader something specific. A
skeleton claims to know the shape; get that wrong and the page visibly jumps on load, which is
worse than the blank moment a spinner would have shown honestly instead.The threshold exists because an indicator’s failure mode below that point isn’t
“unnecessary” — it’s actively worse than nothing. A shape that appears and vanishes before a
reader can register what it was reads as a flicker, not as helpful feedback withheld; the fix is
omission, not a faster or smaller version of the same indicator.
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.
Last modified on September 17, 2026
Was this page helpful?
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Invoca","colors":{"primary":"#00A87F","light":"#00A87F","dark":"#0EEEB7"},"logo":{"light":"https://mintcdn.com/invoca-5bd45748/9Md68DqArcq3WD3L/logo/light.svg?fit=max\u0026auto=format\u0026n=9Md68DqArcq3WD3L\u0026q=85\u0026s=3ed629c5be28d08bd1a6580e72b6dbfa","dark":"https://mintcdn.com/invoca-5bd45748/9Md68DqArcq3WD3L/logo/dark.svg?fit=max\u0026auto=format\u0026n=9Md68DqArcq3WD3L\u0026q=85\u0026s=b64ce8e5cfa4f8c7893b2738e8131f45","href":"https://www.invoca.com/"},"favicon":"https://mintcdn.com/invoca-5bd45748/9Md68DqArcq3WD3L/favicon.png?fit=max\u0026auto=format\u0026n=9Md68DqArcq3WD3L\u0026q=85\u0026s=c846713bb1afdf274154a01d7e9abb8d","appearance":{"default":"light"},"background":{"decoration":"gradient"},"navbar":{"primary":{"type":"button","label":"Invoca Academy","href":"https://academy.invoca.com/"}},"footer":{"socials":{"linkedin":"https://www.linkedin.com/company/invoca-com","youtube":"https://www.youtube.com/channel/UCLQwNQFnN1OV1ACnlXICPKw"}},"fonts":{"body":{"family":"Gt America","source":"https://cdn.prod.website-files.com/5d82e225060d003d65ddae98/677d87009656fca63c0c7d8f_GT-America-Standard-Regular.woff2","format":"woff2"}},"contextual":{"options":["copy","download-pdf","view","assistant","chatgpt","claude","perplexity"],"display":"header"},"metadata":{"timestamp":true},"styling":{"latex":true},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"invoca-5bd45748","hasAudioNarration":false,"actualSubdomain":"invoca-5bd45748","customDomains":["docs.invoca.com"],"gitSource":{"type":"github","owner":"invoca","repo":"docs","deployBranch":"main","contentDirectory":"","isPrivate":true,"mountPath":"","label":""},"chroma":{"liveDeploymentHistoryId":"6ae8bdbaca78e468455a6b97","collectionId":"54b26950-9181-41ce-985a-32d474ce8dd0","schemaVersion":"v3","latestBuiltDeploymentHistoryId":"6ae8bdbaca78e468455a6b97"},"feedback":{"thumbs":true,"agentFeedback":true,"contextualFeedback":{"allowEmail":false}},"entitlements":{"AI_CHAT":{"status":"ENABLED"},"REMOVE_BRANDING":{"status":"ENABLED"},"CONTEXTUAL_FEEDBACK":{"status":"ENABLED","enabled":"ENABLED"},"CODE_SNIPPET_FEEDBACK":{"status":"ENABLED"},"WEB_MCP":{"status":"ENABLED"},"NARRATION":{"status":"ENABLED"}},"buildId":"6ae8bdbaca78e468455a6b97","clientVersion":"0.0.4045","searchFilterCounts":[{"language":"default","tag":"Invoca Docs","count":666},{"language":"default","tag":"Guide","count":665},{"language":"default","tag":"Developer","count":191},{"language":"default","tag":"Integrations","count":184},{"language":"default","tag":"Invoca Design System","count":163},{"language":"default","tag":"API Documentation","count":151},{"language":"default","tag":"Campaign \u0026 Call Handling","count":127},{"language":"default","tag":"Insights \u0026 Analytics","count":101},{"language":"default","tag":"Network Integration","count":99},{"language":"default","tag":"Components","count":75}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":true,"enableContextualStarterQuestions":true,"enableAskAiSearchBar":true,"deflection":{"enabled":true,"type":"link","url":"https://support.invoca.com/s/contactsupport"}},"basePath":"","hasChatPermissions":true,"hasWebMcpPermissions":true,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"byok-sep-25","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-updated-9-15","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[{"filePath":"style.css","content":".custom-card .card img{width:60%;padding:1rem;margin:0 auto}a[href=\"https://academy.invoca.com/\"] span.absolute{background-color:#004030}#assistant-entry,#assistant-entry-mobile{background-color:#00a87f;border-color:#00a87f}#assistant-entry-mobile:hover,#assistant-entry:hover{background-color:#009973}#assistant-entry span,#assistant-entry svg,#assistant-entry-mobile span,#assistant-entry-mobile svg{color:#fff}.ivk-home{--ivk-forest:#0A231E;--ivk-footer-bg:#004030;--ivk-accent:#00A87F;--ivk-ink:#1F2121;--ivk-muted:#626464;--ivk-line:#E7E9E4;--ivk-bg:#FFFFFF;--ivk-sand:#F8FAF1;--ivk-banner:#D1FFD1;--ivk-card:#FFFFFF;--ivk-radius-btn:6px;--ivk-radius-card:16px;color:var(--ivk-ink);width:100%;max-width:1600px;margin:0 auto;padding:2rem clamp(1.25rem,4vw,4rem) 4rem}.dark .ivk-home,:root[data-theme=dark] .ivk-home{--ivk-bg:#10201B;--ivk-card:#16261F;--ivk-ink:#EAF1EC;--ivk-muted:#9BAAA1;--ivk-line:#223229;--ivk-sand:#16261F;--ivk-banner:#1C3A2C}.ivk-home a{color:inherit;text-decoration:none}.ivk-home footer a[href*=mintlify]{color:#9ea3a2}.dark .ivk-home footer a[href*=mintlify]{color:#6f7573}.ivk-home footer a[href*=mintlify]:hover{color:#4f5553}.dark .ivk-home footer a[href*=mintlify]:hover{color:#ced3d1}.ivk-home img,.ivk-home svg{display:block}.ivk-home-announce,.ivk-home-hero{border-radius:var(--ivk-radius-card)}.ivk-home-announce{background:var(--ivk-banner);color:var(--ivk-ink);font-size:13.5px;font-weight:500;text-align:center;padding:9px 16px;margin-bottom:20px}.ivk-home-announce a{color:var(--ivk-footer-bg);font-weight:600;text-decoration:underline;margin-left:6px}.ivk-home-hero{position:relative;overflow:hidden;background:var(--ivk-forest);color:#fff;padding:56px 32px}.ivk-home-hero-eyebrow{display:inline-flex;align-items:center;gap:8px;color:#fff;font-size:13px;font-weight:500;border:1px solid rgba(255,255,255,.28);border-radius:999px;padding:6px 14px;margin:0 0 22px}.ivk-home-hero h1{font-size:clamp(28px,4.2vw,44px);font-weight:700;line-height:1.1;letter-spacing:-.01em;max-width:17ch;margin:0 0 16px;color:#fff}.ivk-home-hero h1 .ivk-home-hl{color:#00b388}.ivk-home-hero-lede{font-size:16.5px;color:rgba(255,255,255,.78);max-width:48ch;margin:0 0 30px;line-height:1.55}.ivk-home-search-field{display:flex;align-items:center;gap:10px;background:#fff;border-radius:var(--ivk-radius-btn);padding:13px 16px;max-width:560px;box-shadow:0 14px 34px rgba(0,0,0,.28);cursor:pointer;border:0;width:100%;text-align:left;font-family:inherit}.ivk-home-search-field input{border:0;outline:0;flex:1;font-size:15px;font-family:inherit;color:#1f2121;background:0 0;pointer-events:none}.ivk-home-search-field input::placeholder{color:#626464}.ivk-home-search-field .ivk-home-icon{color:#626464;flex:none}.ivk-home-search-field kbd{font-size:11.5px;color:#626464;border:1px solid #e7e9e4;border-radius:5px;padding:2px 6px}.ivk-home-popular{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}.ivk-home-popular .ivk-home-label{color:rgba(255,255,255,.62);font-size:13px;margin-right:4px}.ivk-home-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.2);color:#fff;font-size:13px;padding:7px 12px;border-radius:999px}.ivk-home-chip:hover{background:rgba(255,255,255,.16)}.ivk-home-note-banner{background:var(--ivk-sand);border:1px solid var(--ivk-line);border-radius:var(--ivk-radius-card);padding:16px 20px;font-size:13.5px;color:var(--ivk-muted);line-height:1.55;margin-top:24px}.ivk-home-note-banner strong{color:var(--ivk-ink)}.ivk-home-band{padding:60px 0}.ivk-home-band-alt{background:var(--ivk-sand);border-radius:var(--ivk-radius-card)}.ivk-home-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:26px;flex-wrap:wrap}.ivk-home-section-head h2,.ivk-home-topic-tile h3{font-size:23px;font-weight:700;letter-spacing:-.01em;margin:0;color:var(--ivk-ink)}.ivk-home-section-head .ivk-home-meta,.ivk-home-section-sub{color:var(--ivk-muted);font-size:13.5px}.ivk-home-section-sub{font-size:14.5px;max-width:62ch;margin:-14px 0 26px;line-height:1.55}.ivk-home-topic-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;align-items:start}@media (min-width:1400px){.ivk-home-topic-grid{grid-template-columns:repeat(4,1fr)}}@media (max-width:860px){.ivk-home-topic-grid{grid-template-columns:repeat(2,1fr)}}@media (max-width:600px){.ivk-home-topic-grid{grid-template-columns:1fr}}.ivk-home-topic-tile{background:var(--ivk-card);border:1px solid var(--ivk-line);border-radius:var(--ivk-radius-card);transition:border-color .15s ease}.ivk-home-topic-tile:hover{border-color:color-mix(in srgb,var(--ivk-accent) 40%,var(--ivk-line))}.ivk-home-t-head{display:block;width:100%;padding:22px 22px 18px;cursor:pointer;background:0 0;border:0;text-align:left;font-family:inherit;color:inherit}.ivk-home-row-top{display:flex;align-items:center;justify-content:space-between;gap:10px}.ivk-home-icon-wrap{width:36px;height:36px;border-radius:10px;background:rgba(0,168,127,.12);color:var(--ivk-accent);display:flex;align-items:center;justify-content:center}.ivk-home-count{font-size:12px;color:var(--ivk-muted);white-space:nowrap}.ivk-home-title-row{display:flex;align-items:flex-start;gap:6px;margin:12px 0 4px}.ivk-home-topic-tile h3{font-size:16.5px;font-weight:600;letter-spacing:-.005em;line-height:1.3;min-height:2.6em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ivk-home-tag-pill{flex:none;margin-top:2px;padding:2px 4px;border-radius:6px;font-size:.65rem;font-weight:700;line-height:13px;color:var(--ivk-accent);background:rgba(0,168,127,.1)}.dark .ivk-home-tag-pill,:root[data-theme=dark] .ivk-home-tag-pill{color:#1dc79a;background:rgba(29,199,154,.15)}.ivk-home-desc{font-size:13.5px;color:var(--ivk-muted);margin:0;line-height:1.5;min-height:3em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ivk-home-expand-cue{display:flex;align-items:center;gap:6px;margin-top:14px;font-size:12.5px;color:var(--ivk-accent);font-weight:600}.ivk-home-expand-cue svg{transition:transform .18s ease}.ivk-home-topic-tile.ivk-home-open .ivk-home-expand-cue svg{transform:rotate(180deg)}.ivk-home-t-panel{border-top:1px solid var(--ivk-line);margin-top:2px;padding:16px 22px 22px}.ivk-home-t-panel ul{list-style:none;margin:0 0 12px;padding:0;display:flex;flex-direction:column;gap:9px}.ivk-home-t-panel li a{font-size:13px;color:var(--ivk-ink);display:flex;align-items:center;gap:7px}.ivk-home-dev-card h3 a:hover,.ivk-home-t-panel li a:hover,.ivk-home-viewall{color:var(--ivk-accent)}.ivk-home-t-panel li a svg{flex:none;opacity:.5}.ivk-home-viewall{font-size:12.5px;font-weight:600}.ivk-home-dev-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}@media (max-width:700px){.ivk-home-dev-grid{grid-template-columns:1fr}}.ivk-home-dev-card{background:var(--ivk-card);border:1px solid var(--ivk-line);border-radius:var(--ivk-radius-card);padding:18px;display:flex;gap:12px;align-items:flex-start}.ivk-home-dev-card .ivk-home-icon-wrap{background:var(--ivk-forest);color:#fff;width:32px;height:32px;border-radius:9px}.ivk-home-dev-card h3{font-size:14.5px;font-weight:600;margin:0 0 3px;color:var(--ivk-ink)}.ivk-home-dev-card p{font-size:13px;color:var(--ivk-muted);margin:0;line-height:1.45}"}],"page":{"pagePath":"invoca-design-system/patterns/loading-and-skeletons","slug":"/invoca-design-system/patterns/loading-and-skeletons","pageMetadata":{"title":"Loading \u0026 skeletons","description":"Matching a loading placeholder to the shape of what's coming — and when to reach for a spinner or a progress bar instead.","icon":"loader","keywords":["loading","skeleton","spinner","progress bar","placeholder","shimmer","wave","pulse"],"href":"/invoca-design-system/patterns/loading-and-skeletons"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"Matching a loading placeholder to the shape of what’s coming — and when to reach for a spinner or a progress bar instead.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,em:`em`,li:`li`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,Heading,Mermaid,Table,Tip}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Mermaid)_missingMdxReference(`Mermaid`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Tip)_missingMdxReference(`Tip`,true);return _jsxs(_Fragment,{children:[_jsx(Heading,{level:`2`,id:`the-problem`,children:`The problem`}),`\n`,_jsx(_components.p,{children:`A page that has nothing to show yet has three honest ways to say so: promise a shape (“here’s\nroughly what’s coming”), promise nothing but activity (“something is happening”), or promise a\nmeasurable amount (“this much is done”). Using the wrong one either misleads the reader about\nwhat’s arriving or wastes the one advantage the right one had.`}),`\n`,_jsxs(_components.p,{children:[`Get the `,_jsx(_components.em,{children:`duration`}),` wrong and the failure is different but just as visible. A loading state that\nflashes for 150ms and vanishes reads as a glitch. One that never resolves reads as a hang, and\nthe reader has no way to tell a slow network from a broken one.`]}),`\n`,_jsxs(_components.p,{children:[`Titan already ships all three primitives — Skeleton, Spinner, ProgressBar — with no decision\nabout which to reach for when, and evidence that the gap has a real cost: three separate\napplications have independently built what appears to be the same `,_jsx(_components.code,{children:`TabLoadingSkeleton`}),`, at 21\ninstances each. That is not three teams making different choices. It is three teams answering\nthe same unanswered question on their own.`]}),`\n`,_jsx(Heading,{level:`2`,id:`decide-first-what-do-you-actually-know`,children:`Decide first: what do you actually know?`}),`\n`,_jsx(Mermaid,{chart:`flowchart TD\n A[A wait begins] --> B{Will it usually resolve\u003cbr/>in well under a second?}\n B -->|Yes, or unknown but typically fast| C[Show nothing.\u003cbr/>Render content when it arrives.]\n B -->|No — expect it to be noticeable| D{Is there a real,\u003cbr/>measurable amount of progress?}\n D -->|Yes — a percentage, a step count,\u003cbr/>a file size| E[ProgressBar, determinate]\n D -->|No| F{Is the shape of the\u003cbr/>incoming content known?}\n F -->|Yes — a table, a card, a form| G[Skeleton, matching that shape]\n F -->|No, or the region is small or inline| H[Spinner]`}),`\n`,_jsx(Tip,{children:_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Skeleton is not the default just because it is the most-used.`}),` It is correct only when the\nshape is known. Reaching for it when the shape is a guess trades a blank region for a\nconfidently wrong one.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`vocabulary`,children:`Vocabulary`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Term`}),_jsx(_components.th,{children:`Also called`}),_jsx(_components.th,{children:`The system uses`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Skeleton`}),_jsx(_components.td,{children:`Placeholder, shimmer, wireframe loader`}),_jsxs(_components.td,{children:[_jsx(_components.strong,{children:`Skeleton`}),` — a placeholder matching the known shape of incoming content`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Spinner`}),_jsx(_components.td,{children:`Loader, throbber`}),_jsxs(_components.td,{children:[_jsx(_components.strong,{children:`Spinner`}),` — activity with no known shape and no measurable progress`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Determinate`}),_jsx(_components.td,{children:`Measured, quantified`}),_jsxs(_components.td,{children:[_jsx(_components.strong,{children:`Determinate`}),` — a wait with a real, reportable amount of progress, shown by ProgressBar`]})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`when-this-applies`,children:`When this applies`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`Content whose eventual layout — line count, widths, roughly how many rows or fields — is\nknown before it arrives.`}),`\n`,_jsx(_components.li,{children:`A wait long enough to notice, and a region large enough that a blank space would read as\nbroken rather than loading.`}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`when-it-doesn’t`,children:`When it doesn’t`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Situation`}),_jsx(_components.th,{children:`Do this instead`}),_jsx(_components.th,{children:`Why`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`The response is usually fast — well under a second`}),_jsx(_components.td,{children:`Nothing. Render the content directly.`}),_jsx(_components.td,{children:`A loading state that flashes and vanishes costs more attention than the wait it covered.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`The shape of what’s coming is unknown, or the space is small and inline`}),_jsx(_components.td,{children:_jsx(_components.a,{href:`/invoca-design-system/components/feedback/spinner`,children:`Spinner`})}),_jsx(_components.td,{children:`A skeleton is a promise about layout. Guessing at one is worse than promising nothing.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`A real percentage, step count, or size is available`}),_jsx(_components.td,{children:_jsx(_components.a,{href:`/invoca-design-system/components/feedback/progress-bar`,children:`ProgressBar`})}),_jsx(_components.td,{children:`A skeleton says “wait”; a progress bar says “how much longer” — use the more specific answer when it exists.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Content already on screen is being refreshed`}),_jsx(_components.td,{children:`The data component’s own loading affordance; keep the existing content visible`}),_jsx(_components.td,{children:`Replacing readable content with grey shapes takes away what the reader was looking at.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`The load has failed or timed out`}),_jsx(_components.td,{children:_jsx(_components.a,{href:`/invoca-design-system/patterns/error-handling`,children:`Error handling`})}),_jsx(_components.td,{children:`A loading state is not an error state. This page does not decide timeout or retry behavior — that pattern does.`})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`structure`,children:`Structure`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Order`}),_jsx(_components.th,{children:`Component`}),_jsx(_components.th,{children:`Role`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{\"data-numeric\":`true`,children:`1`}),_jsx(_components.td,{children:`Loading region (any container)`}),_jsxs(_components.td,{children:[`Carries the “this is loading” state — `,_jsx(_components.code,{children:`aria-busy=\"true\"`}),` — which is what gets announced, not the placeholder inside it`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{\"data-numeric\":`true`,children:`2`}),_jsxs(_components.td,{children:[_jsx(_components.a,{href:`/invoca-design-system/components/feedback/skeleton`,children:`Skeleton`}),` — one instance per line, block, or shape`]}),_jsx(_components.td,{children:`Matches the width, count, and spacing of the content it stands in for`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{\"data-numeric\":`true`,children:`3`}),_jsx(_components.td,{children:`Minimum-display and timeout guard (behavioral — not a component)`}),_jsx(_components.td,{children:`Bounds how briefly and how long the loading state may run before handing off`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{\"data-numeric\":`true`,children:`4`}),_jsxs(_components.td,{children:[`Resolved state — real content, `,_jsx(_components.a,{href:`/invoca-design-system/components/data-display/empty-state`,children:`EmptyState`}),`, or `,_jsx(_components.a,{href:`/invoca-design-system/components/feedback/alert`,children:`Alert`})]}),_jsx(_components.td,{children:`What the loading state always ends in. Never itself.`})]})]})]}),`\n`,_jsx(CodeBlock,{numberOfLines:`8`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`8`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`┌─────────────────────────────────────────────┐`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`│ Campaigns [ + New ] │ ← page chrome; not greyed out`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`├─────────────────────────────────────────────┤`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`│ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ │`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`│ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ │ ← skeleton rows, matching the`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ │ real table's column widths`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`└─────────────────────────────────────────────┘`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})}),`\n`,_jsx(Heading,{level:`2`,id:`behavior`,children:`Behavior`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`State`}),_jsx(_components.th,{children:`Behavior`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Wait begins`}),_jsx(_components.td,{children:`If expected duration is well under a second, show nothing at all`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Wait exceeds the threshold`}),_jsx(_components.td,{children:`Skeleton (shape known), Spinner (shape unknown), or ProgressBar (value known) appears`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Content arrives`}),_jsx(_components.td,{children:`Loading state is replaced; the region announces once that it finished, not that it was loading`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Content arrives but is empty`}),_jsxs(_components.td,{children:[`Resolves to `,_jsx(_components.a,{href:`/invoca-design-system/components/data-display/empty-state`,children:`EmptyState`}),`, never to blank space`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Wait exceeds a timeout`}),_jsxs(_components.td,{children:[`Hand off to `,_jsx(_components.a,{href:`/invoca-design-system/patterns/error-handling`,children:`Error handling`}),` — this page does not define the threshold or the retry affordance`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`A refresh of existing content`}),_jsx(_components.td,{children:`Existing content stays visible; no skeleton replaces it`})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`constraints`,children:`Constraints`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`ID`}),_jsx(_components.th,{children:`Constraint`}),_jsx(_components.th,{children:`Rationale`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-01`})}),_jsx(_components.td,{children:`Match the indicator to what’s known: a real value → ProgressBar; a known content shape → Skeleton; neither → Spinner.`}),_jsx(_components.td,{children:`Each primitive communicates a different amount of information. Using the wrong one either overstates what’s known or wastes information that was available.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-02`})}),_jsx(_components.td,{children:`Show no loading indicator at all for a wait expected to resolve in under roughly 300ms.`}),_jsx(_components.td,{children:`Below that, the indicator itself becomes the most noticeable thing on screen — it appears and disappears faster than the reader can register what it was, which reads as a flicker rather than feedback.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-03`})}),_jsx(_components.td,{children:`A skeleton screen matches the real layout — same line count, widths, and spacing as the content it replaces.`}),_jsx(_components.td,{children:`Its only value is preparing the reader for a shape. A mismatch makes the page jump on load, which undoes the reason to use one at all.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-04`})}),_jsxs(_components.td,{children:[`One animation, one loading primitive, per region. Do not mix Skeleton’s `,_jsx(_components.code,{children:`pulse`}),` with `,_jsx(_components.code,{children:`wave`}),`, or a skeleton with a spinner, in the same view.`]}),_jsx(_components.td,{children:`Two loading rhythms in one region read as two different systems, not one thing loading.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-05`})}),_jsxs(_components.td,{children:[`A loading state always resolves — to content, an `,_jsx(_components.a,{href:`/invoca-design-system/components/data-display/empty-state`,children:`EmptyState`}),`, or a handoff to `,_jsx(_components.a,{href:`/invoca-design-system/patterns/error-handling`,children:`Error handling`}),`. It never simply stops appearing.`]}),_jsx(_components.td,{children:`An indefinite loading state is a hang the interface is disguising as progress.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-06`})}),_jsx(_components.td,{children:`A skeleton screen used on more than one page is built once and shared, not re-implemented per application.`}),_jsxs(_components.td,{children:[`Three independent copies of the same `,_jsx(_components.code,{children:`TabLoadingSkeleton`}),`, 21 instances each, are the measured cost of not doing this — see `,_jsx(_components.a,{href:`#known-issues`,children:`Known issues`}),`.`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-07`})}),_jsxs(_components.td,{children:[`Under `,_jsx(_components.code,{children:`prefers-reduced-motion: reduce`}),`, pass `,_jsx(_components.code,{children:`animation={false}`}),` to every Skeleton on the screen.`]}),_jsxs(_components.td,{children:[`The component does not honor the preference itself (see `,_jsx(_components.a,{href:`#known-issues`,children:`Known issues`}),`). This is a call-site workaround, not a fix.`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.strong,{children:`TITAN-LOADING-08`})}),_jsxs(_components.td,{children:[`The loading region carries `,_jsx(_components.code,{children:`aria-busy`}),` and announces completion once; the placeholder itself is hidden from assistive technology.`]}),_jsx(_components.td,{children:`Grey shapes carry no information to a screen reader. The announcement belongs to the region, and only on completion.`})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`content`,children:`Content`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Element`}),_jsx(_components.th,{children:`✅`}),_jsx(_components.th,{children:`❌`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Skeleton itself`}),_jsx(_components.td,{children:`No text at all`}),_jsx(_components.td,{children:`”Loading…” written inside a placeholder shape`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Completion announcement`}),_jsx(_components.td,{children:`”Campaigns loaded” — stated once, when content arrives`}),_jsx(_components.td,{children:`Repeated or continuous “Loading, loading, loading”`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Timeout message`}),_jsxs(_components.td,{children:[`Handed to `,_jsx(_components.a,{href:`/invoca-design-system/patterns/error-handling`,children:`Error handling`}),` for the actual copy`]}),_jsx(_components.td,{children:`Silent indefinite animation with nothing to read`})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`accessibility`,children:`Accessibility`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`A skeleton is decorative; the region it sits in is not.`}),` Hide skeleton shapes from assistive\ntechnology and put `,_jsx(_components.code,{children:`aria-busy=\"true\"`}),` on the region that’s loading. Announce once, when the\ncontent resolves — not the fact that loading started, and not on every render while it’s still\nin progress. A screen-reader user given a running commentary of “loading” has been told less\nthan one given a single, timed “done.”`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Reduced motion is currently unhandled at the component level.`}),` Skeleton’s `,_jsx(_components.code,{children:`pulse`}),` and `,_jsx(_components.code,{children:`wave`}),`\nanimations do not respond to `,_jsx(_components.code,{children:`prefers-reduced-motion: reduce`}),` — the same absence recorded at the\nmotion-foundation level generally, where seven durations exist and none has a reduced variant\n(see `,_jsx(_components.a,{href:`#known-issues`,children:`Known issues`}),`). Until the component handles this itself, pass\n`,_jsx(_components.code,{children:`animation={false}`}),` at every call site under that preference.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`A spinner or progress bar needs an explicit accessible name at the call site.`}),` Neither sets\none automatically — pair a Spinner with `,_jsx(_components.code,{children:`aria-describedby`}),` on the region it describes and\n`,_jsx(_components.code,{children:`aria-busy`}),` on that region; do the same for a ProgressBar reporting a specific process’s state.`]}),`\n`,_jsx(Heading,{level:`2`,id:`variations`,children:`Variations`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Variation`}),_jsx(_components.th,{children:`When`}),_jsx(_components.th,{children:`Change`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Table skeleton`}),_jsx(_components.td,{children:`Loading a list or table`}),_jsx(_components.td,{children:`Skeleton rows only — column headers stay, because they’re already known`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Card skeleton`}),_jsx(_components.td,{children:`Loading a card or tile grid`}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`rounded`}),`/`,_jsx(_components.code,{children:`rectangular`}),` shapes matching the card’s image, title, and body regions`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Form skeleton`}),_jsx(_components.td,{children:`Loading a form’s fields`}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`text`}),`-shape skeletons at the label and field positions, matching real field geometry`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Inline refresh`}),_jsx(_components.td,{children:`Content already on screen is being refetched`}),_jsx(_components.td,{children:`No skeleton. Existing content stays visible; use the data component’s own loading affordance instead`})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`anti-patterns`,children:`Anti-patterns`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The flashing skeleton.`}),` Showing a placeholder for a wait that resolves in 150ms turns a fast\nresponse into a visible flicker. The fix is not a faster skeleton — it’s not showing one at all\nunder the ~300ms threshold.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The eternal skeleton.`}),` Nothing about Skeleton times out on its own. Without a timeout in the\ncalling code, a failed or hung request looks identical to “still loading,” and the reader waits\nfar longer than they should before concluding otherwise.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Two animations in one screen.`}),` One region pulsing while another waves reads as two systems\nloading independently, not one page. Titan ships both animations with no decision between them —\npick one and stay consistent within a screen.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Rebuilding the skeleton screen per application.`}),` The pattern this page exists to prevent has\nalready happened three times over — see `,_jsx(_components.a,{href:`#known-issues`,children:`Known issues`}),`. A composed skeleton\nscreen worth naming is worth sharing.`]}),`\n`,_jsx(Heading,{level:`2`,id:`known-issues`,children:`Known issues`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`ID`}),_jsx(_components.th,{children:`Kind`}),_jsx(_components.th,{children:`What`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.a,{href:`/invoca-design-system/foundations/open-decisions#titan-gap-31`,children:`TITAN-GAP-31`})}),_jsx(_components.td,{children:`Open decision`}),_jsxs(_components.td,{children:[`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). Three applications have independently built the same `,_jsx(_components.code,{children:`TabLoadingSkeleton`}),`.`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.a,{href:`/invoca-design-system/foundations/open-decisions#titan-gap-08`,children:`TITAN-GAP-08`})}),_jsx(_components.td,{children:`Open decision`}),_jsxs(_components.td,{children:[`Reduced motion is unimplemented at the token layer — seven durations exist and none has a reduced variant, which is why `,_jsx(_components.a,{href:`#constraints`,children:`TITAN-LOADING-07`}),` is a call-site workaround rather than a fix.`]})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`gaps-in-the-current-rules`,children:`Gaps in the current rules`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`Where the exact line sits between “well under a second” (show nothing) and “noticeable” (show\nan indicator) — this page names the shape of the threshold without a specific number beyond\nthe ~300ms already stated in Constraints.`}),`\n`,_jsx(_components.li,{children:`What a ProgressBar should do when the underlying measurement stalls — hold its last known\nvalue, or show some “no progress” signal distinct from a hang.`}),`\n`,_jsx(_components.li,{children:`Whether a skeleton screen, once built once and shared, should live as a Titan export or stay\nan application-level composition — the constraint says “shared,” not where.`}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`why-it-works-this-way`,children:`Why it works this way`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The decision runs on what’s actually known, not on which primitive looks best, because each of\nthe three makes a different claim and a wrong claim costs the reader something specific.`}),` A\nskeleton claims to know the shape; get that wrong and the page visibly jumps on load, which is\nworse than the blank moment a spinner would have shown honestly instead.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The threshold exists because an indicator’s failure mode below that point isn’t\n“unnecessary” — it’s actively worse than nothing.`}),` A shape that appears and vanishes before a\nreader can register what it was reads as a flicker, not as helpful feedback withheld; the fix is\nomission, not a faster or smaller version of the same indicator.`]}),`\n`,_jsx(Heading,{level:`2`,id:`related`,children:`Related`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.a,{href:`/invoca-design-system/components/feedback/skeleton`,children:`Skeleton`}),`,\n`,_jsx(_components.a,{href:`/invoca-design-system/components/feedback/spinner`,children:`Spinner`}),`, and\n`,_jsx(_components.a,{href:`/invoca-design-system/components/feedback/progress-bar`,children:`ProgressBar`}),`, the three primitives this\npattern chooses between. `,_jsx(_components.a,{href:`/invoca-design-system/patterns/error-handling`,children:`Error handling`}),`, the\nhandoff once a wait exceeds its timeout. `,_jsx(_components.a,{href:`/invoca-design-system/patterns/empty-and-zero-states`,children:`Empty \u0026 zero states`}),`,\nwhat a load resolves to when it succeeds with nothing to show.`]}),`\n`,_jsx(Heading,{level:`2`,id:`status`,children:`Status`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`First-pass proposal, not established policy.`}),` No audited Invoca screen backs this page —\nTitan’s design library was not available while writing it. It is built from general\ninteraction-design practice and from what `,_jsx(_components.a,{href:`/invoca-design-system/components/feedback/skeleton`,children:`Skeleton`}),`,\n`,_jsx(_components.a,{href:`/invoca-design-system/components/feedback/spinner`,children:`Spinner`}),`, and\n`,_jsx(_components.a,{href:`/invoca-design-system/components/feedback/progress-bar`,children:`ProgressBar`}),` already establish.`]}),`\n`,_jsxs(_components.p,{children:[`It also sits on top of a real, verified finding, not a hypothetical one:\n`,_jsx(_components.a,{href:`/invoca-design-system/foundations/open-decisions#titan-gap-31`,children:`TITAN-GAP-31`}),`. Skeleton — the\ncomponent this page leans on most — carries `,_jsx(_components.strong,{children:`no theme overrides, no tokens, and no\nrepresentation in the design library at all`}),`, despite being the most-used component in this\narea: `,_jsx(_components.strong,{children:`132 usages across 13 applications`}),`. This page proposes how to `,_jsx(_components.em,{children:`use`}),` Skeleton\nconsistently. It does not, and cannot, resolve what Skeleton `,_jsx(_components.em,{children:`is`}),` — that gap stays open, and\neverything below is a usage layer built on top of an undecided foundation.`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Loading \u0026 skeletons","description":"Matching a loading placeholder to the shape of what's coming — and when to reach for a spinner or a progress bar instead.","icon":"loader","keywords":["loading","skeleton","spinner","progress bar","placeholder","shimmer","wave","pulse"],"href":"/invoca-design-system/patterns/loading-and-skeletons"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"The problem","slug":"the-problem","depth":2,"children":[]},{"title":"Decide first: what do you actually know?","slug":"decide-first-what-do-you-actually-know","depth":2,"children":[]},{"title":"Vocabulary","slug":"vocabulary","depth":2,"children":[]},{"title":"When this applies","slug":"when-this-applies","depth":2,"children":[]},{"title":"When it doesn’t","slug":"when-it-doesn’t","depth":2,"children":[]},{"title":"Structure","slug":"structure","depth":2,"children":[]},{"title":"Behavior","slug":"behavior","depth":2,"children":[]},{"title":"Constraints","slug":"constraints","depth":2,"children":[]},{"title":"Content","slug":"content","depth":2,"children":[]},{"title":"Accessibility","slug":"accessibility","depth":2,"children":[]},{"title":"Variations","slug":"variations","depth":2,"children":[]},{"title":"Anti-patterns","slug":"anti-patterns","depth":2,"children":[]},{"title":"Known issues","slug":"known-issues","depth":2,"children":[]},{"title":"Gaps in the current rules","slug":"gaps-in-the-current-rules","depth":2,"children":[]},{"title":"Why it works this way","slug":"why-it-works-this-way","depth":2,"children":[]},{"title":"Related","slug":"related","depth":2,"children":[]},{"title":"Status","slug":"status","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":[],"subdomain":"invoca-5bd45748","siteName":"Invoca","title":"Loading \u0026 skeletons - Invoca","relatedPagesConfig":{"enabled":true,"limit":5,"mode":"automatic"},"contextualStarterQuestions":[{"_id":"6ab71c03fc74c9b83ba92d16","deploymentId":"6a10d9fbb2a094386ec2a44e","questionText":"What's the difference between spinners?","status":"active","generatedBy":"contextual","path":"invoca-design-system/patterns/loading-and-skeletons","createdAt":"2026-09-26T01:12:35.876Z","updatedAt":"2026-09-26T01:12:35.876Z","__v":0},{"_id":"6ab71c03fc74c9b83ba92d15","deploymentId":"6a10d9fbb2a094386ec2a44e","questionText":"How do I choose a loading state?","status":"active","generatedBy":"contextual","path":"invoca-design-system/patterns/loading-and-skeletons","createdAt":"2026-09-26T01:12:35.876Z","updatedAt":"2026-09-26T01:12:35.876Z","__v":0},{"_id":"6ab71c03fc74c9b83ba92d14","deploymentId":"6a10d9fbb2a094386ec2a44e","questionText":"When should I use a skeleton?","status":"active","generatedBy":"contextual","path":"invoca-design-system/patterns/loading-and-skeletons","createdAt":"2026-09-26T01:12:35.876Z","updatedAt":"2026-09-26T01:12:35.876Z","__v":0}],"navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-09-17T18:53:35.855Z","gitLastModified":"2026-09-17T18:53:06.000Z","originalFileLocation":"invoca-design-system/patterns/loading-and-skeletons.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"light\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Matching a loading placeholder to the shape of what\u0026#x27;s coming — and when to reach for a spinner or a progress bar instead.\">\u003cmeta data-mint-head name=\"application-name\" content=\"Invoca\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Loading \u0026amp; skeletons - Invoca\">\u003cmeta data-mint-head property=\"og:description\" content=\"Matching a loading placeholder to the shape of what\u0026#x27;s coming — and when to reach for a spinner or a progress bar instead.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Invoca\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://invoca-5bd45748.mintlify.app/_mintlify/api/og/v2?division=Patterns\u0026amp;title=Loading+%26+skeletons\u0026amp;description=Matching+a+loading+placeholder+to+the+shape+of+what%27s+coming+%E2%80%94+and+when+to+reach+for+a+spinner+or+a+progress+bar+instead.\u0026amp;theme=1c2016f9c5ca3323f39e5905\u0026amp;signature=7c58f985e773e7b05afea49f76b7f62c4658fb6eeced512c3d44083843696265\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Loading \u0026amp; skeletons - Invoca\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Matching a loading placeholder to the shape of what\u0026#x27;s coming — and when to reach for a spinner or a progress bar instead.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://invoca-5bd45748.mintlify.app/_mintlify/api/og/v2?division=Patterns\u0026amp;title=Loading+%26+skeletons\u0026amp;description=Matching+a+loading+placeholder+to+the+shape+of+what%27s+coming+%E2%80%94+and+when+to+reach+for+a+spinner+or+a+progress+bar+instead.\u0026amp;theme=1c2016f9c5ca3323f39e5905\u0026amp;signature=7c58f985e773e7b05afea49f76b7f62c4658fb6eeced512c3d44083843696265\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/invoca-5bd45748/Q9AjSeskElHvFPzD/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Invoca\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#00A87F\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/invoca-design-system/patterns/loading-and-skeletons.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.invoca.com/#organization\",\"name\":\"Invoca\",\"url\":\"https://docs.invoca.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/invoca-5bd45748/9Md68DqArcq3WD3L/logo/light.svg?fit=max\u0026auto=format\u0026n=9Md68DqArcq3WD3L\u0026q=85\u0026s=3ed629c5be28d08bd1a6580e72b6dbfa\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.invoca.com#website\",\"name\":\"Invoca\",\"url\":\"https://docs.invoca.com\",\"publisher\":{\"@id\":\"https://docs.invoca.com/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons#webpage\",\"url\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons\",\"name\":\"Loading \u0026 skeletons\",\"description\":\"Matching a loading placeholder to the shape of what's coming — and when to reach for a spinner or a progress bar instead.\",\"dateModified\":\"2026-09-17T18:53:06.000Z\",\"isPartOf\":{\"@id\":\"https://docs.invoca.com#website\"},\"breadcrumb\":{\"@id\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Patterns\",\"item\":\"https://docs.invoca.com/invoca-design-system/patterns/overview\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Loading \u0026 skeletons\",\"item\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons#article\",\"headline\":\"Loading \u0026 skeletons\",\"name\":\"Loading \u0026 skeletons\",\"description\":\"Matching a loading placeholder to the shape of what's coming — and when to reach for a spinner or a progress bar instead.\",\"url\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons\",\"mainEntityOfPage\":{\"@id\":\"https://docs.invoca.com/invoca-design-system/patterns/loading-and-skeletons#webpage\"},\"dateModified\":\"2026-09-17T18:53:06.000Z\",\"keywords\":\"loading, skeleton, spinner, progress bar, placeholder, shimmer, wave, pulse\",\"publisher\":{\"@id\":\"https://docs.invoca.com/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.invoca.com#website\"}}]}\u003c/script>","siteName":"Invoca","nav":{"global":null,"products":[{"product":"Invoca Docs","pages":[{"title":"Invoca Docs","href":""}]},{"product":"Developer","pages":[{"title":"Developer","href":"/en/latest"}]},{"product":"Invoca Design System","icon":"palette","color":{"light":"#00b388","dark":"#00b388"},"description":"","tabs":[{"tab":"Get started","icon":"rocket","pages":[{"title":"Get started","href":"/invoca-design-system"}]},{"tab":"Foundations","icon":"palette","pages":[{"title":"Foundations","href":"/invoca-design-system/foundations/overview"}]},{"tab":"Content","icon":"font","pages":[{"title":"Content","href":"/invoca-design-system/content/voice-and-tone"}]},{"tab":"Components","icon":"shapes","pages":[{"title":"Components","href":"/invoca-design-system/components/overview"}]},{"tab":"Patterns","icon":"clipboard-list","groups":[{"group":"Patterns","root":{"title":"Patterns overview","icon":"layout-template","href":"/invoca-design-system/patterns/overview"},"directory":"card","pages":[{"title":"Destructive confirmation","icon":"trash-2","href":"/invoca-design-system/patterns/destructive-confirmation"},{"title":"Form validation","icon":"clipboard-check","href":"/invoca-design-system/patterns/form-validation"},{"title":"Empty \u0026 zero states","description":"Nothing here yet, versus nothing matched — and why a failed load is neither.","icon":"inbox","href":"/invoca-design-system/patterns/empty-and-zero-states"},{"title":"Loading \u0026 skeletons","icon":"loader","href":"/invoca-design-system/patterns/loading-and-skeletons"},{"title":"Error handling","description":"Where a deterministic action or load failure is shown, and what it must tell the reader next.","icon":"octagon-alert","href":"/invoca-design-system/patterns/error-handling"},{"title":"Filtering and search","icon":"filter","href":"/invoca-design-system/patterns/filtering-and-search"},{"title":"Bulk selection","icon":"list-checks","href":"/invoca-design-system/patterns/bulk-selection"},{"title":"Inline editing","icon":"pencil","href":"/invoca-design-system/patterns/inline-editing"},{"title":"Progressive disclosure","icon":"chevrons-down-up","href":"/invoca-design-system/patterns/progressive-disclosure"},{"title":"Notifications","icon":"bell","href":"/invoca-design-system/patterns/notifications"},{"title":"Footers","icon":"panel-bottom","href":"/invoca-design-system/patterns/footers"},{"group":"CRUD","root":{"title":"CRUD overview","icon":"database","href":"/invoca-design-system/patterns/crud/overview"},"pages":[{"title":"Create","icon":"circle-plus","href":"/invoca-design-system/patterns/crud/create"},{"title":"Read","icon":"eye","href":"/invoca-design-system/patterns/crud/read"},{"title":"Update","icon":"pencil-line","href":"/invoca-design-system/patterns/crud/update"}]}]}]},{"tab":"Views","icon":"table-columns","pages":[{"title":"Views","href":"/invoca-design-system/views/overview"}]},{"tab":"AI Experience","icon":"robot","pages":[{"title":"AI Experience","href":"/invoca-design-system/ai-experience/overview"}]},{"tab":"Contribute","icon":"code-pull-request","pages":[{"title":"Contribute","href":"/invoca-design-system/contribute/governance"}]},{"tab":"Open Issues","icon":"triangle-exclamation","pages":[{"title":"Open Issues","href":"/invoca-design-system/foundations/divergences"}]}]}]},"basePath":"","title":"Loading \u0026 skeletons - Invoca","buildId":"dpl_36EDyZCDfRuoUD9af5CH4M1bqY2h","deploymentId":"dpl_36EDyZCDfRuoUD9af5CH4M1bqY2h","dynamics":[],"chunks":[]}