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.

LayoutPatternBest for
autoMirrors the geometry of real child elements.Existing content blocks.
cardImage, avatar, title, meta, and small label ghosts.Card grids and post feeds.
listAvatar with two text lines repeated by row count.Search results, inbox rows, contacts.
articleLarge media block followed by staggered text lines.Articles, blog posts, documentation pages.
profileLarge 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()),
});
Press a button to see skeleton in action

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()),
  });
}
OptionTypeDefaultDescription
messagestring"Failed to load"Error text shown inside the error layer.
retrybooleanfalseShows a retry button.
onRetryFunctionnullRuns 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 valueTypeDescription
refReact.RefObjectAttach to the skeleton host with ref=.
show()() => voidActivates skeleton on the ref element.
hide()() => voidRemoves the skeleton overlay.
error(opts?)(opts?) => voidShows the error layer on the ref element.
wrap(asyncFn, opts?)PromiseRuns 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

MethodPurpose
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

BehaviorDescription
Ghost overlayInjects an absolutely positioned overlay of .fa-sk-ghost spans.
ResizeObserverRebuilds the overlay while the active host changes size.
MutationObserverActivates new [data-skeleton] elements after mount().
ARIA managementTemporarily applies loading-oriented ARIA values and restores previous values on hide.
Error layerInjects .fa-sk-error-layer with icon, message, and optional retry button.
Idempotent callsDuplicate 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.

EventCancelableWhen it fires
fa.skeleton.showYesBefore skeleton activates.
fa.skeleton.shownNoAfter skeleton activates.
fa.skeleton.hideYesBefore skeleton deactivates.
fa.skeleton.hiddenNoAfter skeleton deactivates.
fa.skeleton.errorNoAfter an error state is shown.
fa.skeleton.retryNoAfter 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

AttributeAllowed valuesDefault
data-skeletonPresent or absentRequired for auto-activation.
data-skeleton-layoutauto, card, list, article, profileauto
data-skeleton-colsInteger ≥ 1Calculated from host width.
data-skeleton-rowsInteger ≥ 12 for card, 5 for list.
data-skeleton-autotrue, falsetrue
data-skeleton-ignorePresent or absent

Class Reference

ClassRole
.fa-sk-wave-overlayInjected overlay container that holds ghost shapes.
.fa-sk-ghostIndividual animated ghost shape.
.fa-sk-error-layerError container with icon, message, and retry button.
.fa-sk-error-msgError message inside the error layer.
.fa-sk-retryRetry 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 require fa-component.
  • Call Skeleton.mount() once on boot for non-React pages.
  • On CDN, initialize with new FrontAlign().Skeleton() and call mount() on the instance.
  • Static methods remain available as FrontAlign().Skeleton.show(), FrontAlign().Skeleton.hide(), FrontAlign().Skeleton.error(), and FrontAlign.Skeleton.wrap().
  • The auto layout 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 useSkeleton instead of calling Skeleton.mount().

FrontAlign