SVG✨ Premium

Spirograph

A rosette of sixteen off-centre rings: half turn one way, half the other, and the two rosettes weave through each other like a spirograph.

CSSJSRotation

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

Recipe app — the cooking timer dial turns during the countdown — Spirograph example 1

① Recipe app — the cooking timer dial turns during the countdown

WhenAt the “Let it simmer” step you start the timer: the copper rosette starts turning around the time left, and the button becomes “Pause” and stops it dead.
WhyThe dial shows at a glance, from across the kitchen, that the timer is running; paused, the rosette freezes without disappearing.
Settingsdata-rings="12" data-hue="18" data-hue-step="4", --spg-base: 120px, --spg-offset: 40px, --spg-speed: 8s, --spg-line: 1.5px, --spg-play set to paused then running on click. The scene handles the countdown.
Watchmaker's blog — header illustration for an article on guilloché — Spirograph example 2

② Watchmaker's blog — header illustration for an article on guilloché

WhenThe article opens on a wide dark strip where a golden 24-ring rosette turns very slowly, cropped top and bottom like a close-up of a dial.
WhyGuilloché engraving is precisely offset circles cut around a centre: the rosette illustrates the article's subject instead of a stock photo.
Settingsdata-rings="24" data-hue="34" data-hue-step="1", --spg-base: 200px, --spg-offset: 62px, --spg-speed: 30s, --spg-height: 176px, --spg-bg: #16212b.
Online course platform — rings around the profile photo while it uploads — Spirograph example 3

③ Online course platform — rings around the profile photo while it uploads

WhenOn the Profile page you pick a new photo: ten violet rings appear and turn around the avatar until the bar reaches 100%, then fade.
WhyFeedback shows exactly where the picture is about to change, and the rings' small offset (10 px) makes a tight halo that does not spill over the form.
Settingsdata-rings="10" data-hue="235" data-hue-step="8", 150 px root (--spg-height: 150px), --spg-base: 96px, --spg-offset: 10px, --spg-speed: 3s, --spg-bg: transparent. The scene handles the fade-in and the progress bar.

How it works

The script creates data-rings rings (16 by default) inside the centre point .spg-stage. Each ring is a 120 px circle (--spg-base) whose centre sits 50 px (--spg-offset) away from the area's centre, at an evenly spread starting angle: 0°, 22.5°, 45°… Stacked, these off-centre circles draw a rosette, the shape a spirograph traces.

Rotation goes through a single animation, spgSpin: translate(-50%, -50%) rotate(angle) translateX(offset), the angle running from its start to start + 360°. Because the circle is off-centre, turning it really makes it travel around the middle. Even rings turn clockwise, odd ones counter-clockwise (.spg-ring--reverse), all at 12 s per turn. So two rosettes of eight rings cross each other: they merge into a sixteen-petal rosette, split, weave diamonds, then meet again.

Colours run from blue to pink: hue 220° plus 10° per ring (data-hue, data-hue-step), with opacity easing down from 0.85 to just under 0.5. The --spg-play variable pauses (paused) or resumes (running) the whole rosette without redrawing it. --spg-grow and --spg-drift, 0 by default, give the rings different sizes and speeds.

Accessibility

  • Decoration: the ring container carries aria-hidden="true". In a timer or a file upload, the remaining time and the state must be written as text (role="timer", aria-live).
  • Reduced motion: under prefers-reduced-motion: reduce, the animation is removed and each ring stays at its starting angle: the full rosette shows, still.
  • No interaction: nothing takes focus, nothing reacts to the pointer. The --spg-play setting lets your own button pause the rotation.
  • Continuous motion: two groups turning in opposite directions create a swirling feel. On a reading page, prefer a slow speed (--spg-speed of 20 to 30 s) or a spot that does not stay in view all the time.

Browser compatibility

CSS plus a dozen lines of JavaScript to create the rings. CSS variables used inside @keyframes and NodeList.forEach set the minimum versions taken from MDN data. Measured in Chromium 151, Firefox 153 and WebKit 26.5: 16 rings per copy, rotation visible in all three engines, two independent copies, still rosette under reduced motion.

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

Without JavaScript the area stays empty. Browsers without CSS variables do not show the rosette.

The code

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

index.html — structure
<div class="fx-0615-root" data-rings="16">
  <div class="spg-stage" aria-hidden="true"></div>
</div>

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

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
data-rings (root) 16 Number of rings (1 to 40), evenly spread. An even number gives two equal rosettes turning in opposite directions.
data-hue / data-hue-step (root) 220 / 10 Hue of the first ring and hue step between two rings (−60 to 60). A step of 1 or 2 gives an almost single-colour rosette.
--spg-base 120px Ring diameter.
--spg-offset 50px Distance between a ring's centre and the area's centre. 0 stacks every ring into a single circle that looks still; larger opens the rosette.
--spg-speed 12s Length of one full turn.
--spg-play running paused freezes the rosette where it is; switching back to running resumes it.
--spg-grow / --spg-drift 0px / 0 Size increase per ring and speed gap per ring (0.1 = each ring 10% slower than the previous one). They break the rosette's symmetry.
--spg-line / --spg-height / --spg-bg 1px / 300px / #0a0a0f Stroke width, area height and background. The rosette stays centred; if it is larger than the area, it gets cropped.

FAQ

Recursive Circles nests circles inside one another and makes them breathe in place. Here, same-size rings are moved off the centre and turn around it in two groups going opposite ways: the rotation is what creates the pattern.
The toy draws one continuous curve (a hypotrochoid). The effect recreates its look with circles offset around a centre, which gives the same rosette while staying light: no canvas, no curve maths.
Change the variable on the root: root.style.setProperty('--spg-play', 'paused'), then 'running' to go on. The rosette resumes exactly where it stopped.