SVG✨ Premium

Recursive Circles

An SVG circle holding three circles, each holding three more, four levels deep; a breathing wave travels from the largest down to the smallest.

CSSJSRecursive

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. SVG has 30 effects, including 3 free. Explore the category →

3 usage examples

“Nested infinities” exhibition at a science centre — the figure beside the headline — Recursive Circles example 1

① “Nested infinities” exhibition at a science centre — the figure beside the headline

WhenThe exhibition page opens: the five-level figure fills the right half, bleeds slightly off the edge, and its breathing keeps flowing down to the smallest circles.
WhyAn exhibition about fractals needs a picture that is one: every circle holds the same figure, smaller, and the wave running through it shows the nesting.
Settingsdata-depth="5" data-branches="3", 326 px root with --rcc-height: 326px, --rcc-bg: transparent, --rcc-stroke: 1.6px, pushed 34 px past the right edge.
Referral scheme of a cooperative grocery — the figure grows with the “levels” slider — Recursive Circles example 2

② Referral scheme of a cooperative grocery — the figure grows with the “levels” slider

WhenThe visitor drags the slider from 0 to 4: the figure goes from one circle to 4, 13, 40 then 121, and the member total updates beside it.
WhyThe “everyone brings in three” rule becomes visible: each new level adds three circles inside every circle of the level before.
SettingsThe slider changes data-depth on the root; data-colors="#14532d,#15803d,#65a30d,#ca8a04,#c2410c", --rcc-bg: #f7f5ec, --rcc-stroke: 1.8px. Until someone touches it, the scene steps through the levels on its own.
Textile pattern editor — a small figure in the “Preparing your PDF” notification — Recursive Circles example 3

③ Textile pattern editor — a small figure in the “Preparing your PDF” notification

WhenClicking “Export as PDF” brings up a dark notification at the bottom of the screen, with a 46 px figure breathing while the file is prepared.
WhyIn a pattern design tool, a progress sign that is itself a repeating pattern feels at home; with only two levels it stays crisp at that size.
Settingsdata-depth="2", data-colors="#f3efe9,#e0a46a,#d0705a", 46 px root (--rcc-height: 46px), --rcc-speed: 1.6s, --rcc-step: .18s, --rcc-stroke: 1.2px. The scene shows and hides the notification.

How it works

The figure is built by a recursive function, grow(). It draws a circle, then places data-branches child circles inside it (3 by default), tangent to each other and to the parent's edge. Their radius comes from an exact formula: r × sin(π/n) / (1 + sin(π/n)), which is 46% of the parent for three children and 50% for two. The function then calls itself on every child, turning half a step at each level, down to data-depth (4 by default). Total: 1 + 3 + 9 + 27 + 81 = 121 circles.

Drawing happens in a fixed coordinate system, viewBox="-112 -112 224 224", centred on 0: the figure always sits in the middle of the area and scales to its smaller side, with no position maths. Each level has its own colour (indigo, violet, magenta, cyan, green, then round again) and stroke width, slightly thinner every generation (computed in CSS from the level) and always measured in screen pixels thanks to vector-effect: non-scaling-stroke.

The rccBreath animation takes every circle from 95% to 100% of its size and from 45% to 100% opacity over 3 seconds. The level acts as a delay: 0.25 s more per generation, so the wave starts on the big circle and moves down to the small ones. Delays are negative (- 100 × duration), so nothing jumps on load. A MutationObserver watches data-depth, data-branches and data-colors: changing any of them redraws the figure with no other call.

Accessibility

  • Decorative figure: the SVG carries aria-hidden="true". If the figure stands for real data (such as members in a network), write that data as text next to it.
  • Reduced motion: under prefers-reduced-motion: reduce, the animation is removed; every circle stays at full size and full opacity, and the whole figure remains readable.
  • Nothing to activate: no focusable element, and the figure reacts to neither mouse nor touch. Depth changes go through the attribute, which your own control (slider, buttons) can drive.
  • High depth: beyond 5 levels with 3 branches, the last circles are under a pixel wide in a 300 px area. They add nothing to the picture and make the page heavier.

Browser compatibility

SVG, transform-box: fill-box (Chrome 64, Firefox 55, Safari 11) and max() for the stroke width (Chrome 79, Firefox 75, Safari 11.1), versions taken from MDN data. Measured in Chromium 151, Firefox 153 and WebKit 26.5: 121 circles per copy, figure centre exactly at the area's centre, two independent copies.

Chrome 79+✓ Full (measured)
Firefox 75+✓ Full (measured)
Safari 11.1+✓ Full (measured on WebKit)
Edge 79+✓ Full (Chrome engine)
Safari iOS 11.3+✓ Not checked on a device
Chrome Android 79+✓ Full (measured at 390 px, touch)

Without JavaScript the SVG stays empty. In a browser without max(), the stroke keeps SVG's default width (1 px) and the figure is still correct.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0611-root" data-depth="4" data-branches="3">
  <svg class="rcc-svg" viewBox="-112 -112 224 224" aria-hidden="true"></svg>
</div>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
data-depth (root) 4 Number of generations below the first circle (0 to 6). Can change at any time: the figure redraws.
data-branches (root) 3 Children per circle (2 to 8). 2 gives pairs alternating horizontal and vertical, 4 a cross-shaped rosette, 6 a crown.
data-colors (root) #6366f1, #8b5cf6, #d946ef, #06b6d4, #22c55e Level colours, comma separated; the list starts over when the depth is longer.
--rcc-stroke 1.5px Stroke width of the first circle; each level loses 15%, never below 0.6 px.
--rcc-speed 3s Length of one full breath.
--rcc-step 0.25s Delay of one level behind the previous one. 0 makes the whole figure breathe together.
--rcc-height / --rcc-bg 300px / #0a0a0f Height and background of the area. The figure takes the smaller of the two dimensions, centred.

FAQ

Yes: the same function calls itself on every child down to the requested depth. Set data-depth="1" then "2" to watch each generation repeat the previous one inside every circle.
Yes, by changing the attribute: root.setAttribute('data-depth', 3). The effect watches that attribute and redraws at once. The same goes for data-branches and data-colors.
Their stroke loses 15% of the starting width at each level (never below 0.6 px), and a thinner line looks paler. The breath, offset from one level to the next, also takes them down to 45% opacity at different moments. With reduced motion, every level sits at full opacity.