Skip to content

Skeleton

FeedbackSince 0.2.0
import { Skeleton } from '@absoluteui/components/skeleton';
import '@absoluteui/components/skeleton/style.css';

Using Angular? See Skeleton for Angular.

Decorative loading placeholders: text lines, rectangles, and circles that reserve layout while content loads. Skeletons are hidden from assistive technology. The loading state is communicated by:

  • Skeleton.markBusy(region, true), which sets aria-busy="true" on the region being loaded;
  • a visually hidden status inside that region, such as a Spinner with its default hidden label.

Sizes are strictly validated CSS lengths written through the CSSOM.

  • Reserving layout for content that is loading, to reduce layout shift.
  • Lists, cards, and profile headers whose shape is known in advance.
  • As the only loading feedback for assistive technology: pair with a visually hidden status (Spinner).
  • Measurable progress: use ProgressBar.
  • Content whose shape is unknown.
import { Skeleton } from '@absoluteui/components/skeleton';
import { Spinner } from '@absoluteui/components/spinner';
const region = document.querySelector<HTMLElement>('#profile');
if (region) {
const avatarHost = document.createElement('div');
const textHost = document.createElement('div');
const statusHost = document.createElement('div');
region.replaceChildren(statusHost, avatarHost, textHost);
Skeleton.markBusy(region, true);
const status = Spinner.create(statusHost, { label: 'Loading profile', delay: 300 });
const avatar = Skeleton.create(avatarHost, { shape: 'circle', width: '3rem' });
const text = Skeleton.create(textHost, { lines: 2 });
const done = (): void => {
status.destroy();
avatar.destroy();
text.destroy();
Skeleton.markBusy(region, false);
};
setTimeout(done, 1000);
}
import { Skeleton } from '@absoluteui/components/skeleton';
const host = document.querySelector<HTMLElement>('#chart-placeholder');
if (host) {
Skeleton.create(host, { shape: 'rect', height: '12rem', animation: 'none' });
}
import { Skeleton } from '@absoluteui/components/skeleton';
import { Spinner } from '@absoluteui/components/spinner';
import '@absoluteui/components/skeleton/style.css';
import '@absoluteui/components/spinner/style.css';
const region = document.querySelector<HTMLElement>('#profile');
if (region) {
const statusHost = document.createElement('div');
const avatarHost = document.createElement('div');
region.replaceChildren(statusHost, avatarHost);
Skeleton.markBusy(region, true);
const status = Spinner.create(statusHost, { label: 'Loading profile', delay: 300 });
const avatar = Skeleton.create(avatarHost, { shape: 'circle', width: '3rem' });
// When loaded:
status.destroy();
avatar.destroy();
Skeleton.markBusy(region, false);
}
import { Skeleton } from '@absoluteui/components/skeleton';
const host = document.createElement('div');
document.body.append(host);
Skeleton.create(host, { lines: 2 });
<div class="aui-skeleton aui-skeleton--text aui-skeleton--pulse" aria-hidden="true">
<span class="aui-skeleton__line"></span>
<span class="aui-skeleton__line"></span>
<span class="aui-skeleton__line aui-skeleton__line--last"></span> (60% wide)
</div>
<div class="aui-skeleton aui-skeleton--rect|circle aui-skeleton--pulse" aria-hidden="true">
<span class="aui-skeleton__block"></span>
</div>
Skeleton.create(host: HTMLElement, options?: SkeletonOptions): Skeleton

Global configuration key: skeleton (see Configuration).

Option Type Default Description
shape 'text' | 'rect' | 'circle' "text" Placeholder shape.
lines number 3 Text lines (integer 1..100); the last of several lines is 60% wide.
width string | null CSS length (px, rem, em, %, ch). null restores the default.
height string | null CSS length (px, rem, em, %, ch); per line for text. null restores the default.
animation 'pulse' | 'none' "pulse" Pulse animation; always none under reduced motion. Globally configurable.
Property Type Description
host HTMLElement The aria-hidden host.
Method Description
Skeleton.markBusy(container: HTMLElement, busy: boolean): void Set or remove aria-busy=“true” on the loading region.
update(options: Partial<SkeletonOptions>): void Merge and apply options.
destroy(): void Remove placeholders, attributes, custom properties, and subscriptions.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-skeleton Host (aria-hidden).
aui-skeleton--text | --rect | --circle Shape modifiers.
aui-skeleton--pulse | --none Animation modifiers.
aui-skeleton__line Text line placeholder.
aui-skeleton__line--last Shorter last line (60%).
aui-skeleton__block Rectangle or circle placeholder.

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-color-active | --aui-color-hover | --aui-easing-standard | --aui-radius-pill | --aui-radius-sm | --aui-space-2

type SkeletonShape = 'text' | 'rect' | 'circle';
type SkeletonAnimation = 'pulse' | 'none';
interface SkeletonConfig { animation: SkeletonAnimation }
interface SkeletonOptions extends Partial<SkeletonConfig> {
shape?: SkeletonShape;
lines?: number;
width?: string | null;
height?: string | null;
}

Pattern: Decorative placeholders (aria-hidden) inside a region marked aria-busy, with a separate status message.

Key Context Action
None Any Not focusable; no keyboard interaction.
Element Role
host none (aria-hidden=true)
Attribute Element Value
aria-hidden host true
aria-busy loading region (Skeleton.markBusy) “true” while loading; removed when done

Not focusable. Placeholders never receive focus.

Criterion Level Note
1.3.1 Info and Relationships A
2.2.2 Pause, Stop, Hide A
2.3.3 Animation from Interactions AAA
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • Placeholders are hidden from assistive technology; the region should contain a visually hidden status (for example a Spinner) that announces loading.
  • The pulse animation is disabled under prefers-reduced-motion and animation: none.
  • aria-busy support varies across screen readers; the status message is the reliable announcement.

pulse, and static (animation: 'none', reduced motion, or forced colors).

From Trigger To
pulse update({ animation: 'none' }) or global config static
pulse prefers-reduced-motion: reduce or common animation: 'none' static (CSS)
any destroy() host restored to empty
  • Skeletons carry no information for assistive technology. Put a visually hidden status in the busy region, for example Spinner.create(statusHost, { label: 'Loading profile' }), so the loading state is announced.
  • aria-busy support varies across screen readers; the status message is the reliable announcement.
  • The pulse is disabled under prefers-reduced-motion: reduce and the common animation: 'none' setting.

Width defaults to 100% of the container and never exceeds it (max-inline-size: 100%). Line height is em-based, so it scales with the surrounding font size.

Logical properties throughout. The shorter last line ends at the inline end of its content, like real text.

Placeholders use color-active and pulse toward color-hover; color-active stays distinct from page and surface backgrounds in every built-in theme. Forced-colors mode uses GrayText without animation.

configureAbsoluteUI({ components: { skeleton: { animation: 'none' } } });

Changes apply to existing instances immediately.

Instance options override global and theme defaults. All options are dynamic.

width and height must match ^\d+(\.\d+)?(px|rem|em|%|ch)$. Values such as calc(1px);background:url(x) are rejected before anything is written. Accepted values are applied as custom properties with style.setProperty (CSSOM, CSP-safe); no style attribute strings are generated.

No persistence, network access, or telemetry.

No DOM listeners; one configuration subscription. Each render replaces at most lines empty spans. The pulse animates background-color only. Target: under 2 ms per Skeleton.create with 100 lines in headless Chromium (not yet covered by the e2e performance suite).

Up to 100 lines per instance. For long lists, render a few skeleton rows rather than one per expected item.

  • lines: 1 renders one full-width line (no shortened last line).
  • A circle without height uses its width as its height.
  • update({ width: null }) restores the default width.
  • Unit-less numbers ('10'), negative values, exponents, and CSS functions are rejected.
Code Category Cause
SKELETON_OPTIONS configuration Invalid shape, lines not an integer 1-100, or width/height not a permitted CSS length.
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION configuration Invalid or unknown configuration.

A failed construction leaves the host untouched.

  • Only px, rem, em, %, and ch lengths are accepted.
  • The pulse is a background-color transition, not a moving shimmer gradient.
  • CSS functions (calc, var, clamp) as sizes.
  • Wave or shimmer gradients.