Loaders✨ Premium

Pulse Loading

Seven bones — avatar, image block 120 px, three lines — breathe in opacity (1 → 0.4 → 1) over 1.5 s ease-in-out; delays 0–400 ms. Pure CSS, zero JavaScript.

CSSPulseCard

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

Mobile news feed — articles waiting to load — Pulse Loading example 1

① Mobile news feed — articles waiting to load

WhenReading app screen (news, blog, social network): several stacked skeleton cards fill the viewport while the server responds.
WhyThe downward pulse wave — each bone 100 ms after the previous — suggests content arriving progressively rather than flashing all at once. On a dark background, the absence of a shimmer avoids the moving reflections that tire the eye during late-night browsing.
SettingsRepeat .pulse-skel three times with a 16 px gap; for each additional card, shift the global animation-delay by +200 ms using a CSS variable --card-offset applied to each bone.
Comments under a playing video — list waiting to load — Pulse Loading example 2

② Comments under a playing video — list waiting to load

WhenVideo watch page: the player is already playing (slow pan, 12:47 / 28:10) and, below it, the comments section shows full-width skeleton cards — 48 px avatar, name, date, three lines — while the comment thread arrives.
WhyUnder a playing video the picture is already moving: a light sweep (fx-0446, fx-0439) would add a second, directional motion that pulls the eye toward the list instead of the video. The opacity pulse has no trajectory — each bone dims in place and comes back — and signals the wait without competing with the player. Rendering-wise only opacity is animated: no gradient to repaint, no pseudo-element per bone; on a phone already decoding video, the five bones of each comment remain a negligible load, unlike the moving-gradient ::after of the neighbouring shimmers.
Settingswidth: 100% and padding: 12px; .pulse-skel-img removed (a comment has no image block); .pulse-skel-title at 40 % and .pulse-skel-subtitle at 22 % (name + date); header bottom margin 12 px; 1.5 s duration kept; --card-offset: .2s on the second comment (FAQ recipe). Without the image block the first line becomes the second child of .pulse-skel and inherits the 90 % of the nth-child(2) selector: a 90 %/100 %/55 % taper, plausible for comment text.
Unrolling notification panel — rows waiting to load — Pulse Loading example 3

③ Unrolling notification panel — rows waiting to load

WhenCollaborative work tool (project management, task tracking): the user clicks the bell, the panel unrolls under the top bar, at the edge of the board they were reading — where a colleague is moving their shared cursor — and its rows — 48 px avatar, title, timestamp, excerpt — breathe while the notifications are fetched.
WhyThe panel opens in peripheral vision, which is most sensitive to motion: a highlight travelling across a row would be noticed without even looking at the panel and would pull the eyes off the board. An in-place opacity change has no direction and no displacement; it says the rows are coming without claiming attention. The 1.5 s rhythm, slower than the run of a sweep, works the same way: the panel waits, the main content stays the subject.
Settingswidth: 100% and padding: 12px 14px; card background and border removed, 1 px divider between rows; .pulse-skel-img removed and a single .pulse-skel-line (the excerpt, 55 % via :last-child); header bottom margin 12 px; --row-offset +150 ms per row; 1.5 s duration kept. The panel must stay darker than the page (here #0c0c14) so that 8 % white bones remain readable.

How it works

The sold HTML contains a .pulse-skel card (280 px wide, 296 px tall, background: rgba(255,255,255,.03), border rgba(255,255,255,.06), border-radius: 16px, padding: 20px) with seven bones. The header (.pulse-skel-header, flex, gap 14 px) places the circle .pulse-skel-avatar (48×48 px) next to a flex‑1 column holding .pulse-skel-title (14 px tall, 65 % of the inner width) and .pulse-skel-subtitle (10 px, 45 %). Below: .pulse-skel-img (100 % × 120 px, border-radius 10 px) then three .pulse-skel-line elements (10 px tall: the first two at 100 %, the last at 55 %). All bones share background: rgba(255,255,255,.08) and border-radius: 6px (50 % for the circle). The .demo-preview wrapper included in the sold HTML is the site's demo frame — leave it out when integrating.

A single @keyframes pulseBreath0 % and 100 % at opacity: 1, 50 % at opacity: 0.4 — applies to all bones: duration 1.5 s, ease-in-out, infinite. Staggered delays control the start of each bone: avatar and title at 0 s, image block at 0.1 s, subtitle at 0.15 s, then the three lines at 0.2 s / 0.3 s / 0.4 s. The 400 ms total spread between the first and last bone produces a downward brightness wave: at t = 750 ms (bottom of the 1.5 s cycle), the image block sits at opacity ≈ 0.40 while the third line has barely begun to dim. No JavaScript — the js field in the sold JSON is empty.

Unlike the shimmer variants in this category — fx-0439 Card Skeleton, fx-0446 Wave Shimmer, fx-0447 Rainbow Shimmer — which animate the position of a gradient on a ::after pseudo-element, Pulse Loading only animates the opacity of each bone's background: no pseudo-element, no gradient, one animation. A CSS bug affects line widths: the selector .pulse-skel-line:nth-child(2) matches nothing — the lines sit at positions 3, 4 and 5 among the direct children of .pulse-skel, not position 2 (which is occupied by .pulse-skel-img). The second line therefore renders at 100 % width instead of the intended 90 %; the expected 100 %/90 %/55 % taper becomes 100 %/100 %/55 %, eliminating the middle step.

Accessibility

  • prefers-reduced-motion not handled: pulseBreath runs unconditionally regardless of the user's preference. Add @media (prefers-reduced-motion: reduce) { .pulse-bone, .pulse-bone-circle { animation: none; opacity: 1; } } to freeze the bones at rest.
  • The .pulse-skel container has neither role="status", nor aria-busy="true", nor any alternative text: screen readers perceive only an empty, silent area. Add role="status" aria-label="Loading…" to .pulse-skel and remove it once the real content appears.
  • The seven bones are empty div elements with no attributes. Set aria-hidden="true" on .pulse-skel (or on each bone) to remove mute nodes from the accessibility tree — the loading information is then carried solely by the container's alternative text.
  • On a light theme, the bone background rgba(255,255,255,.08) on a white surface becomes invisible. Use a CSS variable — for instance --pulse-bone-bg — and adapt its value per theme; rgba(0,0,0,.07) is a reasonable starting point for light backgrounds.

Browser compatibility

Pure CSS (no JavaScript): requires @keyframes and animation (IE 10+), flex (IE 11 partial), border-radius and rgba(). No pseudo-elements, no gradients, no transforms.

Chrome 21+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
IE 11✓ Full (flex without -ms-)
Mobile iOS/Android✓ Full

Without CSS animation (IE 9), the seven bones display at opacity: 1 and no longer pulse — the skeleton remains visible and static, which is still a coherent fallback for a loading indicator.

The code

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

index.html — structure
<div class="pulse-skel">
  <div class="pulse-skel-header">
    <div class="pulse-bone-circle pulse-skel-avatar"></div>
    <div style="flex:1">
      <div class="pulse-bone pulse-skel-title"></div>
      <div class="pulse-bone pulse-skel-subtitle"></div>
    </div>
  </div>
  <div class="pulse-bone pulse-skel-img"></div>
  <div class="pulse-bone pulse-skel-line"></div>
  <!-- 2 more .pulse-skel-line -->
</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
Bone color (CSS — .pulse-bone, .pulse-bone-circle { background }) rgba(255, 255, 255, .08) Brightness of each bone at full opacity. rgba(255,255,255,.12) for higher contrast on a very dark background; rgba(0,0,0,.07) for a light theme.
Minimum opacity (CSS keyframe — 50% { opacity: .4 }) 0.4 Depth of the breath: 0 = full fade-out at mid-cycle (blink), 0.7 = barely noticeable variation. Between 0.3 and 0.5 it reads as breathing; below 0.2 it reads as blinking.
Cycle duration (CSS — animation: 1.5s …) 1.5 s Breathing speed. 0.8 s = fast, sense of urgency; 2.5 s = slow and soft, suited to wellness interfaces. Update the value in both the generic selector and the delay overrides.
Maximum delay (CSS — last override animation-delay: .4s) 400 ms (third line) Wave amplitude: 0 ms = all bones pulse in sync; 800 ms = very pronounced wave but the last bone lags visibly behind the first. Redistribute intermediate delays proportionally.
Card width (CSS — .pulse-skel { width: 280px }) 280 px Container width. Switch to 100% for a fluid card in a grid; percentage-width bones (width: 65%) adapt automatically.
Image block height (CSS — .pulse-skel-img { height: 120px }) 120 px Image preview area. 60 px for a chart widget; 200 px for image-first content; remove the HTML element if your interface has no image block.
Second-line width fix (CSS — .pulse-skel-line:nth-child(2)) 90 % — inactive (broken selector) The current selector matches nothing: replace it with .pulse-skel > .pulse-skel-line:nth-child(4) (4th child of .pulse-skel, i.e. the 2nd line) to restore the 100 %/90 %/55 % taper.

FAQ

Shimmers animate the position of a gradient on a ::after pseudo-element: a light sweeps across the bone horizontally. Pulse Loading only animates the opacity of the bone's own background: no pseudo-element, no gradient, no horizontal motion. The result is fewer rendering layers, shorter code, and a look that creates no directional reflection — suited to interfaces where a moving shimmer would feel too active (night mode, OLED screens where black pixels are off).
The selector .pulse-skel-line:nth-child(2) targets an element that is both a .pulse-skel-line and the second child of its parent. In the sold code's structure, the second direct child of .pulse-skel is .pulse-skel-img, not a line: the lines sit at positions 3, 4 and 5. The selector therefore matches nothing and the width: 90% rule is never applied. Fix: .pulse-skel > .pulse-skel-line:nth-child(4) { width: 90% }.
Repeat the .pulse-skel HTML as many times as needed and apply a growing base delay to each card via a custom property. Example: .pulse-skel:nth-child(2) { --card-offset: 200ms }, then on each bone animation-delay: calc(var(--delay-bone, 0s) + var(--card-offset, 0s)). Card 2 starts 200 ms after card 1, adding an inter-card wave on top of each card's internal wave.