Bouncing Squares
Three 15 px squares, #6366f1 fill, no border radius — rising 20 px with 80% compression at the peak, in a 100 ms cascade. Pure CSS, no 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 .loader-squares container is a single-row flex with gap: 8px and no explicit height. Its three span children are each 15 × 15 px, filled #6366f1, border-radius: 0 — sharp-cornered squares, not circles. Measured in Chromium: 14.77 to 15.00 px depending on the animation phase at capture time. No CSS variables, no JavaScript.
The squareBounce keyframe applies two transforms simultaneously at 50% of the cycle: translateY(-20px) and scale(.8), returning to translateY(0) scale(1) at both ends. Measured at peak in Chromium, the matrix shows scale(0.800) and a vertical offset of −20 px: the square moves up exactly 20 px and occupies 12 × 12 px at that point. The scale(.8) is not a depth effect — it is the squash-and-stretch technique: an elastic object decelerating at the top of its arc compresses slightly, and this single value reproduces that without shadows or secondary elements.
The positive delays 0 s, 0.1 s and 0.2 s stagger the three squares by 100 ms each. Because positive CSS delays only apply on the first cycle, subsequent cycles overlap in a permanent cascade: at steady state, the first square is at its peak when the second is halfway up and the third is leaving the ground. The eye reads a left-to-right wave at 1 Hz (one full cycle per second).
Accessibility
- prefers-reduced-motion not handled: measured with
reducedMotion: 'reduce'emulation, all three animations remain in therunningstate. The code contains no@media (prefers-reduced-motion: reduce)rule. To respect the preference, add@media (prefers-reduced-motion: reduce) { .loader-squares span { animation: none; } }— the squares become static, an honest fallback for a loading indicator. - role, aria-live and alternative text are absent: for a functional loading indicator, add
role="status" aria-live="polite"to.loader-squaresand a<span class="sr-only">Loading…</span>inside it; remove it when loading ends. If the loader is purely decorative (visible text already indicates the state), setaria-hidden="true"on the container. - Contrast:
#6366f1on#0a0a0f(site background) = 4.5:1 — above the 3:1 threshold for non-text graphical components (WCAG 1.4.11). Verify with your own background if you change the color. - Integration: the three
spanelements form a 61 × 15 px box at rest (measured: 3 × 15 px + 2 × 8 px = 61 px). During the bounce, squares extend 20 px upward — allow at least 35 px of height in the parent container and avoidoverflow: hiddenon it.
Browser compatibility
CSS animations and 2D transforms: core properties available in all major browsers since 2012. No JavaScript, no CSS variables, no dependencies.
Without CSS animation support (@keyframes), the three squares remain still at their resting position. No content is lost, but the indicator stops signaling — add alternative text if the loading state is functional.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="loader-squares">
<span></span>
<span></span>
<span></span>
</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 |
|---|---|---|
Color (CSS — background: #6366f1) |
#6366f1 | Fill color for all three squares. Change the value on .loader-squares span to update them all at once. For different colors per square, add :nth-child(1), (2), (3) rules with their own background. |
Size (CSS — width: 15px; height: 15px) |
15 × 15 px | Square dimensions. Change both together to keep the square shape. 8 px for a button; 24 px for a large indicator. The bounce height is absolute — scale translateY proportionally. |
Rise height (CSS — translateY(-20px)) |
−20 px | Maximum height at 50% of the cycle, in absolute pixels. With 8 px squares, -10px keeps the proportions. -30px with 15 px squares lengthens the arc without changing the duration, making the motion read as faster. |
Peak compression (CSS — scale(.8)) |
0.8 (80% of full size) | Scale factor at mid-animation. scale(1) removes the squash-and-stretch: squares rise without shrinking — more mechanical. scale(.6) exaggerates the compression and makes the animation more expressive. |
Cycle duration (CSS — animation: 1s) |
1 s | Duration of one full bounce per square. .7s gives a brisk rhythm; 1.4s a slow, steady motion. The relative stagger (100 ms out of 1 s = 10% of the cycle) stays proportional if you also update the delays. |
Stagger between squares (CSS — animation-delay: .1s and .2s) |
0 s / 0.1 s / 0.2 s | 100 ms gap between each square, i.e. 10% of the cycle. .15s and .3s widen the cascade (15%); 0s for all three synchronizes the bounces into a single simultaneous jump. |
Spacing (CSS — gap: 8px) |
8 px | Space between squares. 4px for a compact group (button); 12px to open up the indicator. Total width at rest: 3 × size + 2 × gap — with 15 px and 8 px, that is 61 px. |
FAQ
scale(.8) at 50% reproduces squash-and-stretch: an elastic object decelerating at the top of its arc compresses slightly. Here, the 80% reduction creates that illusion without any shadow or extra layer. Remove the scale and the squares rise in a straight line: visually crisper, less lively.border-radius: 50%), fx-0433 uses squares (border-radius: 0). Transform: fx-0422 runs from scale(0) to scale(1) — dots appear and disappear — while fx-0433 combines translateY with scale(.8) and never reduces to zero. Timing: fx-0422 uses negative delays (−0.32 s, −0.16 s) that enter steady state on the first frame; fx-0433 uses positive delays (0.1 s, 0.2 s) that produce a visible staggered start on first load.<span></span> to the HTML and a rule .loader-squares span:nth-child(4) { animation-delay: .3s } (and :nth-child(5) { animation-delay: .4s } for five). The base CSS does not need changing: every span inherits from .loader-squares span. Total width: 4 × 15 px + 3 × 8 px = 84 px; 5 × 15 px + 4 × 8 px = 107 px.