Skeleton
Skeleton keeps an interface stable while content is loading. It replaces the real content area with lightweight ghost shapes, then removes them when the data is ready.
Use it for cards, feeds, articles, profile panels, dashboards, and any async region where layout shift would feel rough.
Getting Started
Add data-skeleton to the element that should show a loading placeholder. Use data-skeleton-layout when you want a predefined pattern.
<div class="card" data-skeleton data-skeleton-layout="card">
<img src="/post.jpg" alt="Post thumbnail" />
<h2>Post title</h2>
<p>Post description goes here.</p>
</div>
import Skeleton from 'frontalign';
Skeleton.mount();
mount() activates existing [data-skeleton] elements and starts a MutationObserver for elements added later.
Layouts
Skeleton includes five layout modes. Use auto when the real content already has measurable children. Use a preset when you want a predictable loading shape before content exists.
| Layout | Pattern | Best for |
|---|---|---|
auto | Mirrors the geometry of real child elements. | Existing content blocks. |
card | Image, avatar, title, meta, and small label ghosts. | Card grids and post feeds. |
list | Avatar with two text lines repeated by row count. | Search results, inbox rows, contacts. |
article | Large media block followed by staggered text lines. | Articles, blog posts, documentation pages. |
profile | Large avatar beside title and detail lines. | User cards and account panels. |
Card Layout
Use card for grid-like loading states where each item has media and short text metadata.
<div
data-skeleton
data-skeleton-layout="card"
data-skeleton-cols="2"
data-skeleton-rows="3"
>
<!-- Cards rendered here after load -->
</div>
List Layout
Use list for vertical rows. Control the amount of generated rows with data-skeleton-rows.
<div data-skeleton data-skeleton-layout="list" data-skeleton-rows="6">
<!-- List rows rendered here after load -->
</div>
Article Layout
Use article for pages with a hero/media area followed by text content.
<article data-skeleton data-skeleton-layout="article">
<!-- Article content rendered here after load -->
</article>
Profile Layout
Use profile for user cards, author panels, account summaries, and team member sections.
<div data-skeleton data-skeleton-layout="profile">
<!-- Profile content rendered here after load -->
</div>
Manual Control
Use show() and hide() when loading is controlled by your own request flow.
import Skeleton from 'frontalign/skeleton';
const el = document.querySelector('.feed');
Skeleton.show(el);
const data = await fetchFeed();
renderFeed(el, data);
Skeleton.hide(el);
Calling show() on an active skeleton is safe. Calling hide() on an inactive skeleton is also safe.
Async Wrapper
wrap() handles the full loading flow: show the skeleton, run an async function, hide the skeleton, and show an error state if the request fails.
import Skeleton from 'frontalign/skeleton';
const el = document.querySelector('.posts');
await Skeleton.wrap(el, () => fetchPosts(), {
message: 'Could not load posts',
retry: true,
onRetry: (el) => Skeleton.wrap(el, () => fetchPosts()),
});
Error State
Use error() when loading fails and the skeleton should be replaced with a message and optional retry action.
import Skeleton from 'frontalign/skeleton';
const el = document.querySelector('.dashboard');
try {
const data = await fetchDashboard();
renderDashboard(el, data);
Skeleton.hide(el);
} catch {
Skeleton.error(el, {
message: 'Failed to load dashboard',
retry: true,
onRetry: (el) => Skeleton.wrap(el, () => fetchDashboard()),
});
}
| Option | Type | Default | Description |
|---|---|---|---|
message | string | "Failed to load" | Error text shown inside the error layer. |
retry | boolean | false | Shows a retry button. |
onRetry | Function | null | Runs when the retry button is clicked. Receives the host element. |
Auto Activation
Skeleton.mount() activates existing skeleton hosts and observes new ones added to the document.
import Skeleton from 'frontalign/skeleton';
document.addEventListener('DOMContentLoaded', () => {
Skeleton.mount();
});
Set data-skeleton-auto="false" when a skeleton host should be skipped during boot and controlled manually.
<div class="deferred-widget" data-skeleton data-skeleton-auto="false">
<!-- Loaded later -->
</div>
Auto Shape Detection
The default auto layout reads the real child elements of the host and creates ghost shapes from their measured geometry.
Inline content such as headings, paragraphs, labels, and links becomes line-shaped ghosts. Block content such as images, inputs, buttons, and tables becomes rectangular ghosts. Fully nested shapes are deduplicated automatically.
Add data-skeleton-ignore to a child element when it should stay outside the overlay detection.
<div data-skeleton data-skeleton-layout="auto">
<img src="/thumbnail.jpg" alt="Thumbnail" />
<h2>Article title</h2>
<button data-skeleton-ignore>Share</button>
</div>
React Usage
For React and Next.js, use useSkeleton. The hook manages the ref element directly, so Skeleton.mount() is not needed.
import { useSkeleton } from 'frontalign/react';
| Return value | Type | Description |
|---|---|---|
ref | React.RefObject | Attach to the skeleton host with ref=. |
show() | () => void | Activates skeleton on the ref element. |
hide() | () => void | Removes the skeleton overlay. |
error(opts?) | (opts?) => void | Shows the error layer on the ref element. |
wrap(asyncFn, opts?) | Promise | Runs the complete async skeleton flow. |
React Async Fetch
import { useEffect } from 'react';
import { useSkeleton } from 'frontalign/react';
export function PostFeed() {
const { ref, wrap } = useSkeleton();
useEffect(() => {
wrap(
() => fetchPosts().then(posts => renderPosts(ref.current, posts)),
{
message: 'Could not load posts',
retry: true,
onRetry: () => wrap(() => fetchPosts().then(posts => renderPosts(ref.current, posts))),
}
);
}, []);
return (
<div ref={ref} data-skeleton data-skeleton-layout="list" data-skeleton-rows="8">
{/* Posts rendered here after load */}
</div>
);
}
React Manual Control
import { useEffect, useState } from 'react';
import { useSkeleton } from 'frontalign/react';
export function ProfileCard({ userId }: { userId: string }) {
const { ref, show, hide } = useSkeleton();
const [profile, setProfile] = useState(null);
useEffect(() => {
show();
fetchProfile(userId)
.then(data => {
setProfile(data);
hide();
})
.catch(() => hide());
}, [userId]);
return (
<div ref={ref} data-skeleton-layout="profile">
{profile && (
<>
<img src={profile.avatar} alt={profile.name} />
<h3>{profile.name}</h3>
<p>{profile.role}</p>
</>
)}
</div>
);
}
Common Patterns
Feed Pattern
import Skeleton from 'frontalign/skeleton';
Skeleton.mount();
const feed = document.querySelector('.post-feed');
const posts = await fetchPosts();
renderPosts(feed, posts);
Skeleton.hide(feed);
Dashboard Pattern
Use wrap() per widget when dashboard sections can fail independently.
import Skeleton from 'frontalign/skeleton';
Skeleton.mount();
document.querySelectorAll('[data-widget]').forEach(widget => {
Skeleton.wrap(widget, () => fetchWidgetData(widget.dataset.widget), {
message: 'Could not load widget',
retry: true,
onRetry: (el) => Skeleton.wrap(el, () => fetchWidgetData(el.dataset.widget)),
});
});
API Reference
| Method | Purpose |
|---|---|
Skeleton.show(el) | Activates skeleton on the given element. |
Skeleton.hide(el) | Deactivates skeleton and reveals the real content. |
Skeleton.error(el, opts?) | Replaces skeleton with an error layer. |
Skeleton.wrap(el, asyncFn, opts?) | Runs show → async function → hide, with automatic error handling. |
Skeleton.mount() | Activates existing hosts and starts the MutationObserver. |
Runtime Behavior
| Behavior | Description |
|---|---|
| Ghost overlay | Injects an absolutely positioned overlay of .fa-sk-ghost spans. |
| ResizeObserver | Rebuilds the overlay while the active host changes size. |
| MutationObserver | Activates new [data-skeleton] elements after mount(). |
| ARIA management | Temporarily applies loading-oriented ARIA values and restores previous values on hide. |
| Error layer | Injects .fa-sk-error-layer with icon, message, and optional retry button. |
| Idempotent calls | Duplicate show() and hide() calls are ignored safely. |
Lifecycle Events
Skeleton dispatches events before and after state changes. fa.skeleton.show and fa.skeleton.hide are cancelable.
| Event | Cancelable | When it fires |
|---|---|---|
fa.skeleton.show | Yes | Before skeleton activates. |
fa.skeleton.shown | No | After skeleton activates. |
fa.skeleton.hide | Yes | Before skeleton deactivates. |
fa.skeleton.hidden | No | After skeleton deactivates. |
fa.skeleton.error | No | After an error state is shown. |
fa.skeleton.retry | No | After the retry button is clicked. |
document.addEventListener('fa.skeleton.shown', (event) => {
const { el } = event.detail;
console.log('Skeleton activated on:', el);
});
document.addEventListener('fa.skeleton.error', (event) => {
const { el, message } = event.detail;
console.log('Skeleton error on:', el, message);
});
Prevent activation by canceling the fa.skeleton.show event.
document.addEventListener('fa.skeleton.show', (event) => {
const { el } = event.detail;
if (el.dataset.skeletonDisabled === 'true') {
event.preventDefault();
}
});
Attribute Reference
| Attribute | Allowed values | Default |
|---|---|---|
data-skeleton | Present or absent | Required for auto-activation. |
data-skeleton-layout | auto, card, list, article, profile | auto |
data-skeleton-cols | Integer ≥ 1 | Calculated from host width. |
data-skeleton-rows | Integer ≥ 1 | 2 for card, 5 for list. |
data-skeleton-auto | true, false | true |
data-skeleton-ignore | Present or absent | — |
Class Reference
| Class | Role |
|---|---|
.fa-sk-wave-overlay | Injected overlay container that holds ghost shapes. |
.fa-sk-ghost | Individual animated ghost shape. |
.fa-sk-error-layer | Error container with icon, message, and retry button. |
.fa-sk-error-msg | Error message inside the error layer. |
.fa-sk-retry | Retry button inside the error layer. |
Accessibility
- Keep real content inside the host so assistive technologies receive useful content after loading.
- Avoid interactive controls inside an active skeleton unless they use
data-skeleton-ignore. - Keep host dimensions stable before and after loading to reduce layout shift.
- Prefer
wrap()for async flows so loading, success, and failure states stay consistent.
Notes
- Skeleton uses
data-skeleton; it does not requirefa-component. - Call
Skeleton.mount()once on boot for non-React pages. - On CDN, initialize with
new FrontAlign().Skeleton()and callmount()on the instance. - Static methods remain available as
FrontAlign().Skeleton.show(),FrontAlign().Skeleton.hide(),FrontAlign().Skeleton.error(), andFrontAlign.Skeleton.wrap(). - The
autolayout measures visible child elements, so the host should be visible and sized before activation. wrap()always clears the active skeleton when the async function settles.- Use
data-skeleton-auto="false"for manually controlled loading regions. - In React, use
useSkeletoninstead of callingSkeleton.mount().