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.
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
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 pulseBreath — 0 % 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:
pulseBreathruns 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-skelcontainer has neitherrole="status", noraria-busy="true", nor any alternative text: screen readers perceive only an empty, silent area. Addrole="status" aria-label="Loading…"to.pulse-skeland remove it once the real content appears. - The seven bones are empty
divelements with no attributes. Setaria-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.
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):
<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>
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 |
|---|---|---|
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
::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)..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% }..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.