Skeleton
Reserve the shape of forthcoming content while keeping loading, failure and recovery understandable.
Placeholder versus task progress
Use a skeleton for a predictable content shape during loading. Keep content containers stable so a finished view does not jump unexpectedly. Avoid a skeleton when there is no known shape or when its detail would imply content that may never exist.
A skeleton is a decorative div. It is not a progressbar, does not fetch anything and has no automatic announcement, timeout or failure state. The surrounding task owns those responsibilities.
Anatomy and variants
| Variant / role | Rendered specification |
|---|---|
| text | Width 100%, height 1rem, --radius-sm |
| circular | --radius-full; supply explicit equal width and height |
| rectangular | Radius 0; explicit height needed for visible area |
| rounded | --radius-md; explicit height needed |
| Surface / motion | --color-muted; 1.5s opacity pulse |
| Reduced motion | Static shape, no pulse |
| Layout | Caller provides dimensions and the final content region |
Working loading, content and failure
Skeletons reserve a content shape. They do not prove a request is progressing.
Local content loading; choose an outcome.
'use client';
import { useRef, useState } from 'react';
import { Button, Skeleton } from '@mobtranslate/ui';
export function SkeletonCommunicationExample() {
const [phase, setPhase] = useState<'loading' | 'ready' | 'error'>('loading');
const reload = useRef<HTMLElement>(null);
function finish(next: 'ready' | 'error') { setPhase(next); reload.current?.focus(); }
return <div data-skeleton-lab className="space-y-4">
<p>Skeletons reserve a content shape. They do not prove a request is progressing.</p>
<div aria-busy={phase === 'loading'} aria-label="Local content example" role="region" className="min-h-36 space-y-3">
{phase === 'loading' ? <div aria-hidden="true" className="space-y-3">
<Skeleton variant="circular" width={48} height={48} />
<Skeleton variant="text" width="70%" />
<Skeleton variant="rounded" height={32} />
<Skeleton variant="rectangular" height={24} width="45%" />
</div> : phase === 'ready' ? <><h3 className="font-bold">Local example content</h3><p>The content is now available. No language entry or recording was invented.</p></> : <p>Simulated loading failure. Try loading the example again.</p>}
</div>
<div className="flex flex-wrap gap-3">
<Button ref={reload} type="button" variant="outline" onClick={() => setPhase('loading')}>Load local content</Button>
{phase === 'loading' && <><Button type="button" onClick={() => finish('ready')}>Simulate content ready</Button><Button type="button" variant="secondary" onClick={() => finish('error')}>Simulate content failure</Button></>}
</div>
<p role="status">{phase === 'loading' ? 'Local content loading; choose an outcome.' : phase === 'ready' ? 'Local content ready.' : 'Local content failed; retry is available.'}</p>
</div>;
}
API reference
| Prop | Contract |
|---|---|
| variant | text | circular | rectangular | rounded; text default |
| width / height | CSS string or React numeric px value; optional |
| style | Caller style wins over width/height shorthand |
| className / native div props / ref | HTMLDivElement; no interactive state |
| aria-hidden / surrounding aria-busy | Authored explicitly; wrapper does not infer them |
| children | Native content allowed, but avoid meaningful data inside a decorative placeholder |
Behavior and accessibility
- Hide decorative shapes from accessibility APIs and expose one concise task-level status.
- Use aria-busy on the content region while content is pending, not on an unrelated whole page.
- When loading finishes, replace shapes with actual source-backed content. Failure replaces the skeleton with an explanation and retry.
- Do not animate an endless placeholder after a request has already failed.
- Keep a stable enabled action/focus destination when simulation outcome controls disappear.
Failure cases
- A circular or rectangular skeleton with no height and no visible area.
- Screen readers encountering a list of meaningless shapes.
- Pulsing placeholders presented as measured progress.
- Fake vocabulary, imagery or recordings used to fill the promised shape.
- A loading state without any path to failure, cancellation or retry.
Verification and release scope
- Operate the checked example with keyboard and pointer; inspect the actual name, role, relationships and focus destination.
- Check both themes, narrow reflow, long labels, reduced motion and forced colors. Text contrast and meaningful graphic contrast are separate checks.
- Test the relevant waiting, completion, failure, cancellation and recovery paths. A local simulation is not a verified backend result.
- Use a named screen-reader/browser pair to verify spoken output. DOM attributes and automated passes do not prove the announcement.
- Retain source, review and permission evidence when applying these controls to a real language workflow. Color and motion cannot establish authority.