Carousels

Radial Burst Gallery

Images burst out from the centre into a ring that slowly orbits; selecting one flies it to the centre while the rest shrink back onto the ring, with keyboard navigation.

Arrange a set of images into a ring that breathes. When it scrolls into view, each card bursts out from the centre onto the ring with a staggered spring, then the whole ring slowly orbits, easing smoothly to a stop when the pointer rests on a card or keyboard focus is inside. Select any card and it flies to the centre with a shared layout animation, growing larger as its caption fades in below, while the rest shrink and dim on the ring; click the centre card again, press Esc or click empty space to send it back. Each card can be an image URL or any React node (for example an image drawn with CSS gradients). The selection supports both controlled and uncontrolled modes.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add radial-burst-gallery.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

180
40
180
96
Active edited usageDefault props
<RadialBurstGallery/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/radial-burst-gallery.en.json

Or, once registries are configured in components.json, install it as @webberui/radial-burst-gallery.

Usage

import { RadialBurstGallery } from "@/components/ui/radial-burst-gallery";

<RadialBurstGallery
  images={[
    { src: "/photos/ridge.jpg", alt: "Ridge in the Mist", caption: "Ridge in the Mist · Hehuan Mountain" },
    { src: "/photos/tide.jpg", alt: "After the Tide", caption: "After the Tide · Gaomei Wetland" },
    { src: "/photos/blue.jpg", alt: "Blue Hour", caption: "Blue Hour · North Coast" },
  ]}
  radius={180}
  period={40}
  onSelectedChange={(index) => console.log("selected", index)}
/>

Props

PropTypeDefaultDescription
imagesRadialBurstGalleryImage[]—The images, laid out clockwise from the top in array order
radiusnumber180Ring radius (px); in narrow containers it shrinks to min(radius, container width/2 − card width)
periodnumber40Seconds per full orbit; when omitted, the --wb-rbg-period CSS variable is read, falling back to 40 seconds
stiffnessnumber180Spring stiffness for the burst and the flight; higher is snappier
cardWidthnumber96Width of the cards on the ring (px)
aspectstring"3/4"Card aspect ratio as "width/height"
centerSizenumber260Width of the centre card once selected (px); shrinks automatically in narrow containers
tiltToCenterbooleanfalseRotate ring cards so their tops point outward; by default they stay upright
selectedIndexnumber | null—Controlled selected index (null means none); when omitted the component manages it internally
defaultSelectedIndexnumber | nullnullInitial selected index in uncontrolled mode
onSelectedChange(index: number | null) => void—Called when the selection changes; the argument is null when deselected
pauseOnHoverbooleantruePause the orbit while the pointer is over a card
classNamestring—Forwarded to the outermost container

RadialBurstGalleryImage

FieldTypeDefaultDescription
srcstring—Image URL; use either this or node — node wins when both are given
altstring—Alternative text, also used as the card button's aria-label (required)
nodeReactNode—Custom content, for example an image drawn with CSS gradients
captionReactNode—Caption shown below the centre card once selected

How it works

  • Geometry: n cards are spread evenly around a circle; card i sits at i/n × 360° plus the ring's rotation, clockwise from the top. Cards only translate, never rotate, unless tiltToCenter is on.
  • Burst: the first time the gallery enters the viewport, each card starts at the centre at scale 0.3 and opacity 0 and springs out to the ring (stiffness 180 by default, damping 18), staggered by 50ms; the spring slightly overshoots the ring before settling. Scrolling away and back does not replay it.
  • Orbit: requestAnimationFrame advances the rotation according to period; while the pointer is over a card, focus is inside the gallery or a card is selected, the speed eases to 0 over 150ms instead of stopping dead. The animation loop stops while the gallery is off screen.
  • Timing variable: when period is not passed, the component reads --wb-rbg-period on its root element (40s or 40000ms both work), so the rhythm can be tuned globally; an explicit period prop takes precedence.
  • Selection: the chosen card flies to the centre and grows to centerSize via Motion's layoutId (prefixed with useId, so several galleries on one page never interfere); the others shrink to 0.8 at 0.6 opacity. Ring cards are positioned with left/top rather than transforms so the shared layout animation measures the correct starting point.
  • Narrow containers: a ResizeObserver measures the container width, shrinking the radius to min(radius, container width/2 − card width) and keeping the centre card no wider than the container minus 32px.

Accessibility

  • The root is role="group" with aria-roledescription="gallery"; each card is a type="button" whose aria-label comes from alt
  • Roving tabindex: Tab stops on a single card, ← / → (and ↑ / ↓) move focus in ring order, Home / End jump to the first and last cards; the ring stops orbiting while focus is inside
  • Enter / Space selects and moves focus to the centre card; Esc, activating the centre card again or clicking empty space deselects and returns focus to the original card
  • On selection an aria-live="polite" region announces the position ("n of total") and the caption (or the alt text when there is no caption)
  • When the user has reduce motion enabled at the system level, there is no burst and no orbit — the cards sit statically on the ring — and selection becomes a cross-fade in the centre with no flight