Navigation✨ Premium

Skeleton Text

Ghost lines animated by a pure CSS shimmer — an elegant loading state in a handful of classes, zero JavaScript required.

CSSLoadingPlaceholder

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Navigation has 36 effects, including 4 free. Explore the category →

3 usage examples

Search results — Titles and excerpts waiting for the API — Skeleton Text example 1

① Search results — Titles and excerpts waiting for the API

WhenAn internal search engine or SPA displays results while the API responds to the query — each entry is reduced to a ghost heading and two excerpt lines, with no image or avatar.
WhyText-only bars at decreasing widths, with no image block whatsoever, expose the pure-text specificity of the effect: the shimmer sweeps a dense typographic surface (link title + excerpt × 5 results) that fx-0469 would not cover — there is nothing else to style but structured textual content.
SettingsHeading at 14 px with a blue tint (rgba(100,150,255,.18) at peak) to suggest a link, two excerpt lines at 100% and 84% (height 9 px), animation-delay staggered by 0.1 s per result, 9 px gap between results, zero image block.
Article page — Body text being fetched — Skeleton Text example 2

② Article page — Body text being fetched

WhenThe article is retrieved client-side (fetch, lazy hydration, Next.js ISR) — the skeleton fills the reading column before the Markdown is parsed and rendered.
WhyLines at decreasing widths across the full column height faithfully reproduce the visual density of long-form text. The shimmer maintains the perception of activity during waits that can exceed 1–2 seconds.
SettingsHeading at 50% width (24 px), 2–3 chained .skeleton-paragraph blocks with margin-bottom: 24px between them, max-width: 660px aligned to the editorial column.
Results table — Rows waiting for API response — Skeleton Text example 3

③ Results table — Rows waiting for API response

WhenAn admin or reporting table displays skeleton rows while the SQL query or REST call returns its data (search, filter, pagination).
WhyConstant-height lines (12 px) with no heading precisely reproduce table row density — users immediately understand they are waiting for data rows, not an image or a card.
SettingsRemove .skeleton-heading, stack .skeleton-line directly in the container, fix height: 12px and reduce margin-bottom to 6 px for table density.

How it works

The animation is driven by a single @keyframes shimmer that moves background-position from 200% 0 to -200% 0. Each line carries a horizontal linear gradient sized at 200% of the element's width (color stops at 25%, 50%, 75%) — as the position scrolls, the bright center zone sweeps across the visible surface from right to left, simulating a highlight scanning across waiting content.

The visual distinction between heading and body lines is handled by two CSS layers. The .skeleton-heading block is 24 px tall with a peak opacity of 0.15 at the gradient center (versus 0.10 for ordinary text lines) — perceptually heavier, like a real heading. Lines inside .skeleton-paragraph inherit a 14 px height and 10 px spacing via a more-specific selector, silently overriding the base values without duplicating the rest of the rules.

Decreasing line widths (100%, 95%, 85%, 40% for the last line) mirror the natural length of real sentences — most lines run nearly full width, the last is short like a paragraph that doesn't reach the end. This is handled by nth-child — adding or removing a .skeleton-line requires no CSS changes.

The temporal cascade is achieved with staggered animation-delay values: 0.1 s for the first line, 0.2 s for the second, 0.3 s for the third. The shimmer doesn't cross all lines simultaneously; it travels downward progressively, creating the impression of an active scan that makes the wait feel less inert. No JavaScript is required — everything is declarative and works as soon as the classes are present in the DOM.

Accessibility

  • prefers-reduced-motion is absent from the code — the shimmer animates continuously regardless of the system preference. Recommendation: add @media (prefers-reduced-motion: reduce) { .skeleton-line, .skeleton-heading { animation: none; } } for WCAG 2.3.3 conformance.
  • The .skeleton-line and .skeleton-heading <div> elements are entirely empty (no text, no interactive role) — screen readers skip them by default.
  • No ARIA attributes are defined in the provided code. To signal a loading state, add aria-busy="true" to the parent container until real content is injected, plus an aria-label="Loading…" to contextualize the state.
  • Line contrast is intentionally low (5%–15% opacity) — unreadable as text, which is the expected behavior for a purely decorative visual placeholder.
  • No keyboard or mouse interaction is handled: the effect is passive and includes no focusable or clickable elements.

Browser compatibility

Relies solely on CSS animations and linear gradients — available in all modern browsers since 2014, zero external dependencies.

Chrome 43+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 14+✓ Full
Mobile iOS✓ Full
Android Chrome✓ Full

If CSS animations are unsupported (very old browser), lines render as static semi-transparent blocks — the loading structure remains understandable without motion.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="skeleton-text-demo">
  <div class="skeleton-heading"></div>
  <div class="skeleton-paragraph">
    <div class="skeleton-line"></div>
    <div class="skeleton-line"></div>
    <div class="skeleton-line"></div>
    <div class="skeleton-line"></div>
  </div>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
animation-duration (1.5s) 1.5s Shimmer cycle duration on .skeleton-line and .skeleton-heading. 1 s = snappy, 2.5 s = very slow — match the expected actual load time to avoid a perceptible loop.
gradient peak opacity (.1 / .15) 0.10 lines · 0.15 heading Maximum brightness of the highlight (stop at 50%). Raise to 0.25–0.35 for a light background or to boost shimmer visibility in a light theme.
gradient base opacity (.05) 0.05 Background color of lines at rest (stops at 25% and 75%). Raise to 0.08–0.12 for a more visible skeleton on a very dark or colored background.
animation-delay cascade (0.1s, 0.2s, 0.3s) 0.1 s per line (nth-child) Offset between each line. Set to 0 s for a synchronous shimmer across all lines; raise to 0.25 s to emphasize the downward scanning effect.
line height (12px / 14px) 12 px · 14 px in .skeleton-paragraph Height of the text bars. Use 10 px for a dense table, 16–18 px to mimic large body text (mobile, large typography).
heading height (24px) 24 px Height of .skeleton-heading. Match the real heading size being replaced: h1 → 32–36 px, h2 → 26 px, h3 → 18–20 px.
block max-width (300px) 300 px Maximum width of .skeleton-text-demo. Set to 100% to fill a fluid container (full-width editorial column, responsive card).
border-radius (4px / 6px) 4 px lines · 6 px heading Bar roundness. 2 px for a table-like look, 999 px for rounded pills — harmonize with the surrounding UI's typography and buttons.

FAQ

No, the effect is 100% CSS. The shimmer animation is fully declared via @keyframes and the animation property. No script is required: copy the HTML and CSS and the animation starts automatically. Compatible with any framework (React, Vue, Svelte, Angular) — add the classes to any element, including dynamically generated components.
Repeat the .skeleton-text-demo block as many times as needed — the CSS has no global state, each instance is self-contained. For a table, remove .skeleton-heading and stack .skeleton-line elements directly in a container <div>, reducing margin-bottom to 6–8 px for table row density.
The provided code does not include a prefers-reduced-motion rule. For WCAG 2.3.3 conformance, add: @media (prefers-reduced-motion: reduce) { .skeleton-line, .skeleton-heading { animation: none; } }. The bars remain visible as static blocks — the loading structure is understandable without animation.