LoadersFree

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.

CSSPlayful
Hover or click the scene to interact

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

Léa Martinonline
Did you get a chance to look at the homepage mockups?
Yes, I opened them this morning.
The full-screen hero looks great.
2:32 PM
Léa is typing…
Write a message…

① Typing indicator inside a messaging bubble

WhenChat interface or conversational assistant: the other party or the AI model is composing a reply.
WhyThree cascading circles are the universal signal for a typing "…" — recognized without a label in any messaging context. The compact footprint (52 × 12 px) fits inside a chat bubble without distorting it.
Settings8 px dots, 5 px gap, color #ffffff on a colored bubble background, 1.2 s duration.
Cabmo Comfort · 4 seats

Finding a driver nearby…

3 drivers contacted · usually answers within 2 min

12 Elm Street Central Station
€14.50 Cancel

② Finding a driver in a ride-hailing app

WhenRide-booking app: the request is out, nearby drivers are being pinged and one of them has to accept. The wait depends on a human decision, not on a computation.
WhyThree cascading dots are the messaging "…": someone is there and is about to answer. That is exactly what this screen has to say — we are waiting for a driver to accept, not for a process to finish. A spinning ring would say "the system is computing", a bar would promise a duration nobody knows. The 0.28 s rest between two waves gives the rhythm of a person composing a reply. Set at 16 px in the middle of the panel, the three dots read on a phone held at arm's length.
Settings16 px dots, 10 px gap, color #ffffff on the dark panel, default 1.4 s duration and 0.16 s stagger, centered above the "Finding a driver nearby…" label, role="status" on the status block.
Lumen Studio Nova-V · video Credits: 42
PromptBrittany coast at sunrise, 6 s

Generating your clip…

Duration varies — usually under a minute.

③ Generation wait state in an AI tool

WhenAfter prompt submission: the interface is waiting for a generation model's response (text, image, audio) for an unpredictable duration.
WhyThe animation loops without depending on a known duration — a progress bar is not an option. The 1.4 s rhythm is slow enough to avoid creating urgency and fast enough to show the system is working.
Settings14 px dots, 10 px gap, color #a78bfa, 1.6 s duration, centered below the empty response area.

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", no aria-label, no aria-live on the container. Screen readers ignore these animated circles. For a loading indicator, add role="status" and aria-label="Loading" on div.loader-dots; for a waiting button, prefer aria-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> (standard clip: rect(0,0,0,0) technique).
  • animation-play-state hard-coded to running: the value is embedded in each span'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.

Chrome 84+✓ Full
Firefox 63+✓ Full
Safari 14.1+✓ Full
Edge 84+✓ Full
iOS Safari 14.5+✓ Full
Android Chrome 84+✓ Full

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.

HTML
<div class="loader-dots">
  <span>
  </span>
  <span>
  </span>
  <span>
  </span>
</div>
CSS
.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);
   }
}
JavaScript (fx-0422)
// Effet CSS pur — pas de JavaScript nécessaire

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
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.