Loaders✨ Premium

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.

CSSWaveOrganique

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

Weather app — forecast being refreshed — Wave Shimmer example 1

① Weather app — forecast being refreshed

WhenA weather app's home screen already knows the current conditions (station reading: temperature, wind, swell, tide) but refreshes its forecast panel — the next-hours curve and the 7-day trend — from the API, taking 1–3 s on every open or city change.
WhyThis sweep is not a flat shimmer: the seven-stop bell-curve gradient (peak 0.14) glides left to right in 2.2 s on a cubic-bezier(.4, 0, .2, 1) curve that speeds it up then brakes it, while scaleY oscillates 1 → 1.02 → 0.98 → 1.01 — a swell rolling across the screen, a gust passing through. On a wind-and-sea subject, the waiting motion rhymes with what it announces; a spinning dot or a pulse says nothing about weather. And it is the 100 px block (the upcoming temperature curve) that makes the undulation readable: ±2 % in height is 2 px on that block, 0.2 px on a 10 px line.
SettingsNative structure kept: 48 px circle = today's icon, 65 % title = day, 45 % subtitle = trend, 100 px block = next-hours curve, three lines at 100 / 88 / 55 % = following days. .wave-skel widened to 100 % of the column (308 px, i.e. 274 px bones) with padding: 16px instead of 20 to fit the screen. 2.2 s duration, gradient and 0 / 0.12 / 0.24 s stagger unchanged. The current-conditions card reuses the skeleton's chrome (rgba(255,255,255,.03) background, .06 border, 16 px radius) so that only the pending panel undulates.
Article thumbnail — news or social feed loading — Wave Shimmer example 2

② Article thumbnail — news or social feed loading

WhenA feed (articles, social posts, e-commerce results) shows skeletons during the initial fetch or infinite scroll.
WhyThe 100 px block represents the article thumbnail; the three lines represent the headline and excerpt. The scaleY ±2 % undulation conveys a sense of content being progressively built, consistent with text arriving in chunks. The neutral white-on-dark tint introduces no color noise into a heterogeneous feed.
SettingsRemove the header (avatar + title/subtitle) and keep only block + lines for a pure thumbnail layout. Set .wave-skel to 100 % width for a full-width vertical list. Duplicate 3 instances with animation-delay: 0s / 0.15s / 0.3s on the first ::after of each.
KPI widget — dashboard waiting for data — Wave Shimmer example 3

③ KPI widget — dashboard waiting for data

WhenA finance, analytics or IoT dashboard fetches its indicators from an external API (1–3 s latency); each widget shows its skeleton individually.
WhyThe 2.2 s speed reads as deliberate (long query or calculation) without becoming annoying. The 100 px block stands for a chart or main value; the lines for secondary metrics. The monochrome neutrality suits data-dense interfaces where color is reserved for real data.
SettingsReduce the block height to 60 px; set .wave-skel to 100 % of the widget width. Lower animation-duration to 1.8 s on ::after for a snappier feel if latency is short (< 1 s).

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', animationPlayState is still running and animationName remains waveFlow. 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" and aria-label="Loading" on .wave-skel, and aria-busy="true" on the parent container while loading. Screen readers will then announce "Loading" when the skeleton appears. Remove these attributes (or flip aria-busy to false) when the real content is inserted.
  • Seven empty elements in the accessibility tree: bones are empty divs with no text or role. If role="status" is placed on .wave-skel, also add aria-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 with rgba(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.

Chrome 87+✓ Full
Firefox 87+✓ Full
Safari 14.1+✓ Full
Edge 87+✓ Full
Chrome < 87✓ Static bones (no shimmer)
Mobile iOS 14.5+✓ Full

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):

index.html — structure
<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>
🔒 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
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

Two CSS differences. First: @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.
No, it is a limitation of the current selectors. The :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.
Yes, with three substitutions. Bones: 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.