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.
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



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.
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):
<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 / property | Default | Effect |
|---|---|---|
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
data-depth="1" then "2" to watch each generation repeat the previous one inside every circle.root.setAttribute('data-depth', 3). The effect watches that attribute and redraws at once. The same goes for data-branches and data-colors.