Bouncing Dots
Three 12 px circles in a row that fade in and out in cascade — scale(0) → scale(1) in 0.56 s, negative delays of 0.16 s between each dot, pure CSS, zero JavaScript.
This effect is free — the Loaders category contains 29 effects total, including 4 free. Effect.Labs has 811 vanilla effects. Explore the category →
Usage examples
Finding a driver nearby…
3 drivers contacted · usually answers within 2 min
Generating your clip…
Duration varies — usually under a minute.
How it works
Three empty <span> elements sit inside a display: flex container with gap: 8 px. Each circle is 12 × 12 px (border-radius: 50%, background #6366f1), giving a measured total footprint of 52 × 12 px. The shared rule .loader-dots span attaches the dotBounce animation: 1.4 s, ease-in-out, infinite. No JavaScript — the effect is entirely declarative.
The dotBounce keyframe moves each circle from scale(0) at 0 %, 80 % and 100 % to scale(1) at 40 %. Peak reached at 0.56 s (40 % × 1.4 s), back to zero before 1.12 s (80 %). Between 1.12 s and 1.4 s all three circles are simultaneously invisible — this 0.28 s rest per cycle is the rhythmic gap between two waves. The animation is a scale pulse: the dots do not move, they disappear and reappear in place.
The cascade is built with negative delays: −0.32 s for the first circle, −0.16 s for the second, no delay for the third. A negative delay places the element mid-cycle at page load — the wave is visible immediately, with no blank opening. Measured result: the first circle peaks at t = 0.24 s, the second at t = 0.40 s, the third at t = 0.56 s. The 0.16 s interval is uniform and produces the left → right ripple.
Accessibility
- prefers-reduced-motion not handled: no
@media (prefers-reduced-motion: reduce)rule in the shipped CSS. The animation loops indefinitely regardless of the system preference — non-compliant with WCAG 2.3.3. Add:@media (prefers-reduced-motion: reduce) { .loader-dots span { animation: none; opacity: 1; } }to freeze the circles at their visible state. - No semantic markers: no
role="status", noaria-label, noaria-liveon the container. Screen readers ignore these animated circles. For a loading indicator, addrole="status"andaria-label="Loading"ondiv.loader-dots; for a waiting button, preferaria-busy="true"on the parent button. - No alternative text: all three
<span>elements are empty. If the loader is the only visual signal of processing, add a visually hidden node readable by screen readers:<span class="sr-only">Loading…</span>(standardclip: rect(0,0,0,0)technique). - animation-play-state hard-coded to
running: the value is embedded in eachspan's animation shorthand. Targeting.loader-dots { animation-play-state: paused }has no effect. To stop via JavaScript (reduced-motion compliance or hiding the loader), explicitly target.loader-dots span { animation-play-state: paused }.
Browser compatibility
Requires CSS Animations, transform and gap in a flex context. Zero dependencies, zero JavaScript.
On Chrome 43–83, Firefox 16–62 and Safari 9–14: the animation runs but gap is not supported in flex context — the three circles appear adjacent with no spacing. On browsers without CSS Animations, the circles display at full size, static.
The code
Copy the three blocks into your page. No dependencies.
<div class="loader-dots">
<span>
</span>
<span>
</span>
<span>
</span>
</div>
.loader-dots {
display: flex;
gap: 8px;
}
.loader-dots span {
width: 12px;
height: 12px;
background: rgb(99, 102, 241);
border-radius: 50%;
animation: 1.4s ease-in-out 0s infinite normal none running dotBounce;
}
.loader-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.loader-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes dotBounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
// Effet CSS pur — pas de JavaScript nécessaire
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Dot size (CSS — .loader-dots span width/height) |
12 px | Total footprint = 3 × size + 2 × gap. At 8 px the loader fits inside a standard button; at 16 px it works as a standalone component. |
Spacing (CSS — .loader-dots gap) |
8 px | Space between the 12 px boxes. Since the circles collapse to a point at the bottom of the cycle, the minimum visual gap between them is always gap + (size × (1 − scale_min)). |
Color (CSS — .loader-dots span background) |
#6366f1 | A single property to change. Check contrast at scale(1): WCAG 1.4.11 requires 3:1 between the dot color and the background. |
Cycle duration (CSS — animation-duration in .loader-dots span) |
1.4 s | All three circles keep their delay ratios automatically. 1.0 s = snappy; 2.0 s = calm. The inter-peak interval stays proportional: 0.16 s / 1.4 s ≈ 11.4% of the cycle. |
Cascade offset (CSS — animation-delay on :nth-child) |
−0.32 s, −0.16 s, 0 s | The 0.16 s gap between each circle produces the ripple. For a wider spread: −0.4 s / −0.2 s / 0 s. Keep the delays negative so the cascade is active from the first frame. |
Timing function (CSS — animation-timing-function) |
ease-in-out | ease-out makes the circles appear quickly and disappear slowly; linear gives a mechanical beat. ease-in-out (default) softens both ends. |
Number of circles (HTML — extra <span> elements + CSS) |
3 | Add a 4th <span> and one rule: .loader-dots span:nth-child(4) { animation-delay: 0.16s; }. The 4th circle peaks 0.16 s after the 3rd. Total width becomes 68 px (4 × 12 + 3 × 8). |
FAQ
The animation is a scale pulse, not a vertical bounce — how does it differ from Bouncing Squares (fx-0433)?
Bouncing Squares applies translateY(−20 px): the squares physically rise and fall. Bouncing Dots applies scale(0) → scale(1): the circles disappear and reappear in place, with no movement. The primary use case for Dots is the messaging-style typing indicator; Squares reads more as a bouncy, playful progress signal. Both cascade but differ in easing and duration (1.4 s ease-in-out vs 1.0 s ease-in-out).
How do I add a fourth circle without changing the existing delays?
Add a <span> after the three existing ones and a single rule: .loader-dots span:nth-child(4) { animation-delay: 0.16s; }. The 4th circle peaks 0.16 s after the 3rd (at t ≈ 0.72 s). The nth-child(1) and nth-child(2) rules are untouched. Total width becomes 68 px.
Why use negative delays instead of positive ones (0 s, 0.16 s, 0.32 s)?
Positive delays create a visible blank on page load: the 3rd circle waits 0.32 s before appearing for the first time. With negative delays, each circle is already mid-cycle when the page renders — the cascade is active from the first frame. Wave Bars (fx-0426) uses the same principle across its five bars with delays from −1.2 s to −0.8 s.