Skeleton Text
Ghost lines animated by a pure CSS shimmer — an elegant loading state in a handful of classes, zero JavaScript required.
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



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-lineand.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 anaria-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.
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):
<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>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| 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
@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..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.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.