Feedback and status

Skeleton
v0.5.4
Ready for Production

Loading state placeholders with pulse animation to indicate content is loading

Prefer AI-assisted development?

This documentation is available via the Catalyst MCP Server. Learn how to connect →

Catalyst Skeleton provides animated placeholders for content that is loading, preserving the page's layout structure while data is being fetched — improving perceived performance with zero configuration.

Preview

Installation

npm install @pmi/catalyst-skeleton
import { Skeleton } from '@pmi/catalyst-skeleton';

Variants

Foreground

The default variant. Use for text, buttons, and interactive elements.

Preview

Background

Use for cards, containers, and large content blocks. Typically used as the outer wrapper with nested foreground skeletons inside. Set animated={false} on the background wrapper to prevent double-pulsing.

Preview

Common Patterns

Text Content

Wrap heading and paragraph elements to preserve the full typographic layout while loading.

Preview

Custom Size

Apply arbitrary dimensions with Tailwind utility classes when the skeleton has no child content.

Preview

Component Children

Wrap interactive components — the skeleton inherits the child's shape automatically using the Radix Slot.

Preview

Precomposed Card

A complete card layout with skeleton states applied to every content region.

Preview

Precomposed Hero

A hero section with multiple nested skeleton regions spanning headings, body copy, and CTAs.

Preview

Precomposed Tabs

A tabbed layout with skeleton states on both tab labels and tab panel content. Because TabsList and TabsContent carry their own ARIA roles (role="tablist" and role="tabpanel"), the entire skeleton structure is wrapped in aria-hidden + inert to remove those roles from the accessibility tree and tab order while loading. See Skeleton inside structural ARIA roles for details.

Preview

Loading Lifecycle

Demonstrates the complete loading lifecycle with proper screen reader support. The inert + aria-hidden wrapper hides skeleton placeholders from AT and keyboard navigation while loading. Once content arrives, the wrapper is removed, focus is moved to the first meaningful element, and the aria-live="polite" region announces the new content to screen readers.

This example also includes the long-running loading announcement pattern: after 3 seconds a separate visually-hidden live region begins periodically announcing "Loading, please wait…" every 10 seconds, keeping screen reader users informed that the application is still working.

Preview

Lead with confidence

Manage timelines and budgets, align teams and stakeholders, and deliver on scope with proven project management techniques.

API Reference

Prop

Type

The component also accepts all standard HTML span element attributes and forwards ref.

Accessibility

Skeleton is designed to meet WCAG 2.2 AA standards.

Screen Reader Behaviour

  • aria-hidden="true" is applied so skeleton placeholders are invisible to assistive technologies — they are purely decorative.
  • tabIndex={-1} removes skeleton elements from the keyboard tab order.
  • When toggling between skeleton and real content via the loading prop, wrap the region in an aria-live container so screen readers announce when content becomes available:
<div aria-live="polite" aria-busy={isLoading}>
  <Skeleton loading={isLoading}>{content}</Skeleton>
</div>

Skeleton inside structural ARIA roles

The Skeleton component applies aria-hidden, tabIndex={-1}, and inert to the element it directly wraps. This is sufficient for most content. However, when skeleton placeholders are placed inside structural components that carry their own ARIA roles — such as TabsList (role="tablist"), TabsContent (role="tabpanel"), or TabsTrigger (role="tab") — those outer role-bearing elements remain exposed to assistive technologies and keyboard navigation even though their content is empty. Screen readers will announce an empty tablist, and the tabpanel containers will be reachable via keyboard, violating WCAG 4.1.2 Name, Role, Value (role-bearing elements with no meaningful content expose misleading state to AT) and WCAG 2.4.3 Focus Order (empty containers should not receive keyboard focus).

Wrap the entire structure in an aria-hidden + inert container, and place aria-busy on a separate outer wrapper that remains visible to AT. Both attributes must not share the same element — placing aria-busy on an aria-hidden element means AT never sees it and the loading signal has no effect:

// ✅ aria-busy visible to AT on the outer wrapper;
// aria-hidden + inert remove role="tablist" / role="tabpanel" from the AT tree and tab order
<div aria-busy="true">
  <div aria-hidden="true" {...{ inert: true }}>
    <TabsRoot>…skeleton content…</TabsRoot>
  </div>
</div>

This ensures no empty tab roles are announced and no skeleton container receives keyboard focus. The same principle applies to any component that renders role="grid", role="listbox", role="menu", or other composite ARIA roles around skeleton content.

Motion

The pulse animation uses the motion-safe:animate-pulse modifier, which automatically disables animation for users who have enabled Reduce Motion in their system preferences (WCAG 2.2 SC 2.3.3 — Level AAA).

Colour Contrast

Skeleton text is visually hidden via !text-transparent and removed from the accessibility tree via aria-hidden="true", so the WCAG 2.2 AA text contrast requirement (SC 1.4.3, 4.5:1) does not apply to the hidden text inside a skeleton. The default placeholder colours (pmi-neutral-50 / pmi-neutral-100 in light mode and their dark counterparts) are chosen to look clearly different from real content — making the loading state visually obvious — without implying any readable text. When overriding colours, choose placeholder tones that remain visually distinct from actual content so users can tell at a glance that the page is still loading.

Choosing the Right Component

Use Skeleton when the shape of the incoming content is known in advance. Choose a different primitive when:

If you need…Use instead
An indeterminate spinner (unknown content shape)Spinner
A determinate progress indicator with a percentageProgress Bar
A page-level loading barLoader
A completion notification after the task finishesToast

On this page