Infinity Loop
Two 30 px ring circles approach by 20 px and shrink to 0.8 at mid-cycle — 14 px of overlap — then separate. Container 80 × 40 px, 2 s, pure CSS.
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 effect relies on a single HTML element: .loader-infinity, an 80 × 40 px block, position: relative. Its two pseudo-elements ::before and ::after form the two circles: 30 × 30 px each, border: 4px solid #6366f1, border-radius: 50%, no fill. ::before is placed at left: 0, ::after at right: 0 — their inner edges are 20 px apart at rest. No top is declared: both circles default to top: 0, leaving 10 px empty at the bottom of the block. No JavaScript is provided.
Two symmetrical @keyframes animate each circle over a 2 s period, linear easing, infinite loop. infinityLeft moves ::before from translateX(0) scale(1) to translateX(20px) scale(.8) at mid-cycle, then back. infinityRight applies the same mechanics to ::after in the opposite direction: translateX(-20px) scale(.8). Both animations start with no delay and remain perfectly synchronous. At t = 1 s, each circle measures 24 px (30 × 0.8) and the two overlap by 14 px — the left centre is at 35 px, the right at 45 px, bounding boxes overlapping across [30, 44]. At t = 0 s and t = 2 s, the inner edges are exactly 20 px apart.
The animation does not trace the figure-eight path of the ∞ symbol: both circles move horizontally at the same level, with no vertical displacement. The 14 px overlap at mid-cycle suggests the central crossing of the infinity sign without reproducing it geometrically. The effect contains no var(--…) property: the render does not depend on the host design system. No @media (prefers-reduced-motion) rule is present in the sold CSS: measured with Playwright emulation (reducedMotion: 'reduce'), both animations stay at 2s running.
Accessibility
- prefers-reduced-motion not in the code: with Playwright emulation active, both animations remain at
2s linear running. Add:@media (prefers-reduced-motion: reduce) { .loader-infinity::before, .loader-infinity::after { animation: none; } }— the two circles remain visible and static, which is an honest fallback for a loading indicator. .loader-infinityis a silentdivwith noroleand no text content. For an active loading indicator, addrole="status"to the parent element and include visually hidden text:<span class="sr-only">Loading…</span>. Remove the whole element — or clear the live region — when the operation is complete.- No focusable element, no keyboard or pointer event. The effect is purely decorative on the interactive DOM side. If
role="status"is set on a parent container, addaria-hidden="true"to.loader-infinityitself to prevent a screen reader from traversing the empty pseudo-elements. - Contrast: the circles are open rings — only the
#6366f1border is visible, no fill. On the dark#0a0a0fbackground of the sold code, the indigo exceeds the 3:1 threshold required for non-text components (WCAG 1.4.11). On a light or coloured background, verify the ratio before integrating.
Browser compatibility
Pure CSS: @keyframes, transform inside keyframes, border-radius: 50% and the animation property without prefix. No CSS variables, no JavaScript.
Without CSS animations (IE 9 and earlier), both circles display at their initial position, static — a pair of motionless rings. There is no JavaScript to disable.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="loader-infinity"></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 |
|---|---|---|
Circle colour (CSS — border: 4px solid #6366f1) |
#6366f1 | Colour of both rings. currentColor inherits the parent text colour — useful inside a coloured button or bubble. The property only targets the border; the circles have no fill. |
Border width (CSS — border-width: 4px) |
4 px | Stroke thickness of the ring. 2px gives a thin wireframe; 6px produces near-solid discs. If you change the circle size, adjust the thickness proportionally. |
Circle size (CSS — width: 30px; height: 30px) |
30 × 30 px | Diameter of each circle. If you increase it, make sure both pseudo-elements fit inside the container — the container is 80 px wide, the two circles occupy 60 px at rest, leaving a 20 px central gap. |
Container size (CSS — .loader-infinity { width: 80px; height: 40px }) |
80 × 40 px | Total footprint of the indicator. Height should be ≥ circle diameter plus the desired visual margin. Add top: calc(50% - 15px) to the pseudo-elements to centre them vertically when you change the height. |
Approach distance (CSS — translateX(20px) / translateX(-20px)) |
20 px (14 px overlap at mid-cycle) | How far each circle slides toward the centre at mid-period. 10px: circles remain tangent, no overlap. 30px: 30 px of overlap, the two circles almost merge. Edit both keyframes together. |
Scale at mid-cycle (CSS — scale(.8)) |
.8 (circles at 24 px at mid-cycle) | Scale factor at the moment of maximum approach. 1: no size change, translation alone creates the effect. .6: circles shrink to 18 px, the impression of 'sinking into each other' is more pronounced. |
Cycle duration (CSS — animation: 2s) |
2 s | Period of one full back-and-forth. Change the value on both pseudo-elements simultaneously. 1s: lively animation, signals a short wait; 3s: slow rhythm, suited to a long generation. |
FAQ
top: 0) with no vertical displacement. They overlap by 14 px at mid-cycle, which suggests the central crossing of the infinity sign, but each circle's path is a simple 20 px left-right oscillation. 'Infinity Loop' refers to the infinite repetition of the cycle, not the ∞ geometry. For a true figure-eight, you would need to add a sinusoidal vertical translation or use an SVG offset-path.top value on the pseudo-elements: they default to top: 0, placing the 30 px circles at the top of the 40 px block — 10 px empty remain at the bottom. To centre them, add top: 5px (or top: calc(50% - 15px)) to .loader-infinity::before, .loader-infinity::after.