Skip to main content
WireKit
Copy for LLM

Skeleton

Animated placeholders shown while content loads. Skeletons reduce perceived loading time by giving users a preview of the layout they'll see once data arrives.

Basic Usage

Text Skeleton

Render N lines of varying width:

Text skeleton — 3 lines
Loading content

Avatar Skeleton

A circular avatar placeholder with two short text lines next to it — matches the common "avatar + name + role" row layout:

Avatar skeleton
Loading content

Card Skeleton

Image area + title + body text — matches a typical content card:

Card skeleton
Loading content

Custom Skeleton

Build your own shape from scratch using the shimmer class wk-skeleton:

Custom shape
Loading content

Animation Modes

Choose how the placeholder animates with the animation prop: shimmer (the default gradient sweep), pulse (a lighter opacity fade — half the GPU layers), or none (a static block, no motion). All modes respect prefers-reduced-motion: reduce.

Shimmer, pulse, and static
Loading content
Loading content
Loading content

Conditional Rendering

Use skeletons as a placeholder until data is loaded:

@if($loading)
    <x-wirekit::skeleton type="card" />
@else
    <x-wirekit::card>
        {{-- real content --}}
    </x-wirekit::card>
@endif

With Livewire, pair with wire:loading:

<div wire:loading wire:target="loadUsers">
    <x-wirekit::skeleton type="avatar" />
    <x-wirekit::skeleton type="avatar" />
    <x-wirekit::skeleton type="avatar" />
</div>

<div wire:loading.remove wire:target="loadUsers">
    @foreach($users as $user)
        {{-- real user card --}}
    @endforeach
</div>

Width & Layout

Skeleton dimensions are controlled via width and height props or Tailwind classes:

{{-- Fixed dimensions via props --}}
<x-wirekit::skeleton width="200px" height="1rem" />

{{-- Tailwind classes --}}
<x-wirekit::skeleton class="w-48 h-4" />

{{-- Full-width bar --}}
<x-wirekit::skeleton class="w-full h-3" />

The variant="circular" variant uses equal width and height for a perfect circle.

How the Shimmer Works

WireKit ships a .wk-skeleton utility in dist/wirekit.css that applies a horizontal gradient via ::after and animates it with @keyframes wk-shimmer. The shimmer is themed via the --color-wk-bg-elevated token, so it automatically blends with both light and dark modes.

You can apply this class to any element to turn it into a skeleton placeholder:

<div class="wk-skeleton bg-[var(--color-wk-bg-muted)] rounded h-4 w-40"></div>

Performance characteristics

Each animation mode has a different cost profile — pick by skeleton density:

  • shimmer (default) animates transform: translateX on a dedicated ::after pseudo-element — a compositor-only property, GPU-accelerated, no main-thread paint or layout cost. It is the richest effect and carries one extra compositor layer per skeleton (the ::after), so it is the most expensive of the three at very high density.
  • pulse removes the ::after layer entirely and runs a single opacity keyframe on the base element instead. Opacity is equally compositor-friendly, but with half the layers per skeleton — the better default for density-bound layouts like data tables of skeleton rows.
  • none removes the pseudo-element and the animation: a static placeholder with zero per-frame work. Only the initial paint costs anything — use it when hundreds of placeholders are visible at once, or when the loading window is too short for motion to register.

Three optimizations apply across the modes:

  • content-visibility: auto on every skeleton wrapper lets the browser skip rendering work for off-screen instances entirely — in all three modes. Long lists of skeletons cost effectively zero per frame for every instance below the fold.
  • will-change: transform on the shimmer's ::after hints the compositor to promote the layer eagerly with the most optimal paint path (shimmer only — pulse and none have no ::after).
  • animation-play-state: paused kicks in automatically when the wrapper's aria-busy flips to false (or a parent's does) — developers who toggle aria-busy when loading completes get a free zero-cost stop signal for shimmer and pulse alike.

Under prefers-reduced-motion: reduce, shimmer and pulse both collapse to a static placeholder — every mode behaves like none for users with motion sensitivity, at the matching cost.

Measured impact of the optimizations on the shimmer mode (Playwright Chromium headless, 4 s sample window):

Density × Variant Before optimizations After optimizations Speedup
100 × card59 fps60 fps≈1.0×
300 × card16 fps60 fps3.75×
100 × table25 fps27 fps1.1×
100 × mixed53 fps56 fps1.1×
1000 × mixed5 fps45 fps9.0×

Rule of thumb: shimmer for hero surfaces and ordinary pages, animation="pulse" once skeleton counts reach the hundreds (half the layers), animation="none" when even a pulse would be visual noise — the optimizations above keep all three cheap, but only none is free.

Best Practices

  • Match layout — the skeleton should mirror the eventual content's shape and proportions, so the transition feels seamless
  • Avoid layout shift — ensure the skeleton's total height matches the real content's height to prevent content jumping on load
  • Don't over-use — skeletons are for initial loads, not for every interaction. For quick state updates, use a spinner or nothing at all

Props

Prop Type Default Description
type string 'text' 'text', 'avatar', 'card', 'custom'
lines int 3 Number of text lines (only for type="text")
animation string 'shimmer' 'shimmer' (gradient sweep), 'pulse' (opacity fade, lighter on the GPU), or 'none' (static)
shimmer bool true Legacy flag — false is equivalent to animation="pulse" (kept for back-compat; prefer animation)
scope string|null null Scoped personalization name

Pulse mode (animation="pulse")

For dense lists of skeletons (50+ on screen at once), the default sweeping gradient costs one GPU layer per .wk-skeleton element. Opt in to pulse mode for a lighter alternative — a single opacity animation on the base element with no ::after pseudo-layer:

<x-wirekit::skeleton type="card" animation="pulse" />

(The legacy :shimmer="false" flag is equivalent and keeps working; prefer the animation prop, which also offers none for a fully static placeholder.)

Visually, pulse mode fades each skeleton between full opacity and --reading-skeleton-pulse-opacity (default 0.5) on a 1.6 s cycle. All modes honor prefers-reduced-motion: reduce, and the animated modes pause when the wrapper flips to aria-busy="false".

Sub-Components

Dedicated sub-components are available for common patterns. They work identically to the type prop variants but allow for clearer, more discoverable markup:

skeleton.text

Skeleton text sub-component
Loading content

skeleton.avatar

Skeleton avatar sub-component
Loading content

skeleton.card

Skeleton card sub-component
Loading content

skeleton.table

Render a table placeholder with configurable rows and columns:

Skeleton table — 5 rows, 4 columns
Loading content
Skeleton table — compact 3×2
Loading content

Sub-Component Props

Sub-component Prop Type Default Description
skeleton.text lines int 3 Number of text lines
skeleton.table rows int 5 Number of body rows
skeleton.table cols int 4 Number of columns

All sub-components accept the scope prop for personalization.

Accessibility

  • The skeleton container carries role="status" + aria-live="polite" — screen readers announce the loading state without interrupting
  • An aria-label="Loading" + sr-only "Loading content" text provide redundant announcements
  • The shimmer animation is disabled when the user's OS has prefers-reduced-motion: reduce set (see dist/wirekit.css). The bars remain visible as solid color placeholders
  • Avoid skeletons for operations that complete in under ~200ms — they can feel like flickering to the user. Use skeletons only for loads expected to take 300ms+

Keyboard Interaction

This component is purely presentational and does not respond to keyboard input.

Pitfalls

  • Don't leave a skeleton on the page after content arrives. WCAG 2.2.2 (Pause, Stop, Hide) — perpetual animation distracts. The component's :loaded="$loaded" prop swaps in real content automatically; bind it.

Design Tokens

Element Token
Shimmer base color --color-wk-bg-muted
Shimmer highlight --color-wk-bg-elevated (60% mix)
Bar radius --radius-wk-md
Avatar radius --radius-wk-full
Animation duration 1.6s (fixed)

Customization

Override defaults without publishing views via config/wirekit.php:

'components' => [
    'skeleton' => [],
],

Further Reading

Was this page helpful?

Voting requires cookies or local storage. What we store