Wave Shimmer
Card skeleton, pure CSS: 7 bones, sweep −200 %→+200 % in 2.2 s, scaleY ±2 % that undulates the glow. Stagger 0 / 120 / 240 ms.
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. Loaders has 29 effects, including 4 free. Explore the category →
3 usage examples



How it works
Measured structure: a .wave-skel container at 280 × 276 px (20 px padding, 16 px border-radius, background rgba(255,255,255,.03)) holds seven bones. A circle .wave-bone-circle.wave-skel-avatar at 48 × 48 px anchors the left side of the header; the adjacent flex:1 div (176 px) contains the title (.wave-skel-title, 114 × 14 px, 65 % of 176 px) and the subtitle (.wave-skel-subtitle, 79 × 10 px, 45 %). Below the header: a .wave-skel-block at 238 × 100 px (10 px border-radius), then three .wave-skel-line elements at 10 px tall with widths 100 % / 88 % / 55 % (238 px base = 280 − 2 × 20 px padding). All bones share background: rgba(255,255,255,.06), position: relative and overflow: hidden, which clips the animated pseudo-element.
Shimmer mechanics: each bone produces a ::after (position: absolute; inset: 0) carrying a seven-stop bell-curve gradient: transparent 0 → .04 → .10 → .14 → .10 → .04 → transparent 100 % on background-size: 200 % 100 %. The waveFlow animation (2.2 s, cubic-bezier(.4, 0, .2, 1), infinite) shifts background-position from −200 % to +200 %: the light band enters from the left, reaches center at mid-cycle, exits to the right. At the same time, scaleY oscillates 1 → 1.02 → 0.98 → 1.01 → 1 in sync with the sweep — this is the undulation that sets this effect apart from a flat shimmer: the glow very slightly narrows and widens vertically on each pass, clipped by the parent's overflow: hidden. Maximum amplitude is ±2 %.
Measured stagger (Playwright, system reducedMotion ignored): avatar 0 s, title 0 s, subtitle 0.12 s, block 0.12 s, first line 0.24 s, second and third lines 0 s. The selectors .wave-skel .wave-bone:nth-child(2)::after and :nth-child(3)::after match any bone that is the second or third child of its direct parent within .wave-skel — the subtitle, as the second child of the flex:1 div, is captured at the same delay as the block. The animation-delay rules applied directly to .wave-bone elements (without ::after) have no effect, since the animation lives only on ::after. Result: three synchronized groups (0 / 0.12 / 0.24 s), not a per-element cascade.
Accessibility
- No prefers-reduced-motion handling — confirmed by measurement: under Playwright's
reducedMotion: 'reduce',animationPlayStateis stillrunningandanimationNameremainswaveFlow. Add:@media (prefers-reduced-motion: reduce) { .wave-bone::after, .wave-bone-circle::after { animation: none } }to freeze bones as static blocks — an honest, readable fallback. - Missing semantic role: for a loading indicator, add
role="status"andaria-label="Loading"on.wave-skel, andaria-busy="true"on the parent container while loading. Screen readers will then announce "Loading" when the skeleton appears. Remove these attributes (or fliparia-busytofalse) when the real content is inserted. - Seven empty elements in the accessibility tree: bones are empty
divs with no text or role. Ifrole="status"is placed on.wave-skel, also addaria-hidden="true"on each bone to prevent NVDA from listing them individually. - No site CSS custom properties (measured: zero
var(--…)): the code works on any page without design-system conflicts. The bone color (rgba(255,255,255,.06)) is hardcoded — on a light background the bones become invisible; replace withrgba(0,0,0,.08)and update the highlight gradient accordingly.
Browser compatibility
The only constraint is inset: 0 (shorthand for top/right/bottom/left), supported from Chrome 87, Firefox 87 and Safari 14.1 (late 2020 – early 2021). On older browsers, ::after is not correctly positioned and the shimmer vanishes; bones remain visible as static gray blocks.
Without inset support (browsers before late 2020), the ::after pseudo-elements do not fill the bones: no shimmer, but the bones remain visible as gray rectangles. Without CSS entirely, the elements are inline and empty — invisible.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="wave-skel">
<div class="wave-skel-header">
<div class="wave-bone-circle wave-skel-avatar"></div>
<div style="flex:1">
<div class="wave-bone wave-skel-title"></div>
<div class="wave-bone wave-skel-subtitle"></div>
</div>
</div>
<div class="wave-bone wave-skel-block"></div>
<div class="wave-bone wave-skel-line"></div>
<!-- … 2 more lines, 3 total -->
</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 |
|---|---|---|
Sweep duration (CSS — animation-duration: 2.2s on ::after) |
2.2 s | Speed of the left-to-right sweep. 1.5 s: lively, urgent feel; 3 s: very gentle, suited to a long expected load. Change the value inside the animation: 2.2s cubic-bezier… declaration. |
Highlight intensity (CSS — gradient stops on ::after) |
Peak 0.14 at center (rgba(255,255,255,.14)) | Brightness of the light band. Raise the peak to .22 for a clearly visible shimmer over images; lower to .08 for a very discreet effect on light backgrounds (also scale the flanking stops proportionally). |
Vertical undulation (keyframes waveFlow — scaleY(1.02) and scaleY(.98)) |
±2 % max | Amplitude of the glow's oscillation. scaleY(1.005): nearly imperceptible, near-flat shimmer; scaleY(1.06): noticeable ripple. Clipped by the parent's overflow: hidden — the bone itself never changes size. |
Block height (CSS — .wave-skel-block { height: 100px }) |
100 px | Height of the main content area. 60 px for a compact chart or KPI value; 160 px for a cover image; 200 px for a video. Border-radius can be reduced to 6 px for a panoramic format. |
Line widths (CSS — .wave-skel-line:nth-child(2) { width: 88% } and :last-child { width: 55% }) |
100 % / 88 % / 55 % | Mirrors the expected text length. Long headline: 95 %; two equal lines: both at 100 %; a short last line: 40 %. |
Stagger delays (CSS — ::after of .wave-skel-header+.wave-bone, :nth-child(2), :nth-child(3)) |
0 s / 0.12 s / 0.24 s | Phase offset of the cascade. For a precise per-element stagger, replace the nth-child selectors with explicit class rules (.wave-skel-subtitle::after { animation-delay: .08s }, etc.) — the current nth-child selectors also capture unexpected elements (see how). |
Bone color (CSS — rgba(255,255,255,.06)) |
White at 6 % opacity | For a light background: rgba(0,0,0,.08) on bones, remove or invert the container background, and adapt the gradient stops to dark-on-light. For a branded palette: hsla(220,70%,60%,.12) and match the gradient channels. |
FAQ
@keyframes waveFlow includes scaleY(1.02) / scaleY(.98) / scaleY(1.01) which fx-0439 does not have — this scaleY on ::after produces the vertical undulation. Second: the highlight gradient has 7 stops (bell curve, peak at .14) vs 3 stops for fx-0439 (peak at .10), giving a more defined light band. Duration and easing also differ: 2.2 s with cubic-bezier(.4, 0, .2, 1) vs 1.8 s with ease-in-out.:nth-child(2)::after and :nth-child(3)::after rules match any bone that is the second or third child of its direct container within .wave-skel. The subtitle, as the second child of the flex:1 div, picks up the 0.12 s delay alongside the block, and lines 2 and 3 have no matching rule so they revert to 0 s. For a per-element cascade, replace nth-child with named classes and a dedicated ::after rule for each.rgba(255,255,255,.06) → rgba(0,0,0,.08). Container background: remove or replace with rgba(0,0,0,.03). Highlight gradient: invert the channels, e.g. rgba(0,0,0,0) → rgba(0,0,0,.04) → rgba(0,0,0,.10) → rgba(0,0,0,.14) → …. Animation timing, stagger and structure remain unchanged.