Loaders✨ Premium

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.

CSSLoop

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

Transfer between two accounts — the two rings link the debited account to the credited one — Infinity Loop example 1

① Transfer between two accounts — the two rings link the debited account to the credited one

WhenThe confirmation screen of a banking app, between the moment the user validates a transfer and the moment the bank confirms the credit: the debited account on the left, the credited account on the right, the operation in between.
WhyA transfer is two accounts and a movement from one to the other — exactly what the two rings draw: one in front of each account, moving toward each other until they overlap by 21 px at the centre, then separating. The scene shows the operation itself, not merely 'working': a spinner or a progress bar does not say between whom it is happening. At 1.5× the sold size (120 × 60 px block, 45 px rings), the block fills exactly the gap between the two cards and the rings read as two distinct entities — shrunk into a button, the same effect collapses into a dot.
Settingswidth: 120px; height: 60px on .loader-infinity, rings 45 × 45 px, border-width: 6px, translateX(30px) / translateX(-30px) in both keyframes — 1.5× the sold 80 × 40 / 30 px / 4 px / 20 px, 21 px overlap at mid-cycle. top: calc(50% - 22.5px) to centre, colour #6366f1, scale(.8) and the 2 s cycle unchanged.
Converting a MOV video to MP4 — the loop links the input format to the output format — Infinity Loop example 2

② Converting a MOV video to MP4 — the loop links the input format to the output format

WhenThe conversion window of a video or office tool during encoding: the source file and its format on one side, the output file and its format on the other, the transformation in between — often several minutes.
WhyA conversion has an input and an output that only touch for the duration of the operation: the left ring carries the colour of the MOV badge, the right ring that of the MP4 badge, and their overlap at mid-cycle is the moment one becomes the other. The progress bar below says how much is left; only the loop says from what to what. The 2 s cycle stays calm during a long encode, where a fast spinner ends up suggesting a freeze. At 2× the sold size (160 × 80 px block, 60 px rings), the shape holds its own next to two 140 px file tiles without crushing them.
Settingswidth: 160px; height: 80px, rings 60 × 60 px, border-width: 8px, translateX(40px) / translateX(-40px) — 2× the sold values, 28 px overlap at mid-cycle. top: calc(50% - 30px), scale(.8) and the 2 s cycle unchanged. One colour per ring: ::before in #cbd5e1 (MOV badge), ::after in #22d3ee (MP4 badge) — two border-color values instead of one.
Sync in progress — import dashboard — Infinity Loop example 3

③ Sync in progress — import dashboard

WhenA settings page or dashboard shows the status of an active synchronisation between two sources (client / server, local / remote, import / export).
WhyTwo circles moving toward each other directly illustrate the concept of 'two entities joining'. A spinner has only one element — it does not represent the relationship between two systems. The 80 × 40 px block fits beside a text label without overflowing a table row.
Settingswidth: 60px; height: 30px, circles 22 × 22 px, colour adapted to the theme, text label 'Syncing…' placed to the right of the block.

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-infinity is a silent div with no role and no text content. For an active loading indicator, add role="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, add aria-hidden="true" to .loader-infinity itself to prevent a screen reader from traversing the empty pseudo-elements.
  • Contrast: the circles are open rings — only the #6366f1 border is visible, no fill. On the dark #0a0a0f background 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.

Chrome 43+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 79+✓ Full
Mobile iOS 9+✓ Full
Android Chrome✓ Full

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):

index.html — structure
<div class="loader-infinity"></div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

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

No. Both circles move horizontally at the same level (same 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.
Spinner (fx-0421): a single element rotating 360° — rotational motion, one actor. Gradient Ring (fx-0435): a single conic-gradient ring rotating — same rotation mechanic, different palette. Infinity Loop (fx-0437): two distinct elements in relative motion toward each other — no rotation, no gradient. It is the only entry in the Loading category whose animation depends on the spatial relationship between two separate components.
The sold code does not declare a 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.