Orbital Image Wheel

Scroll-driven half-wheel image layout pinned to the bottom area of the viewport, with GSAP focus effects and animated active captions.

Installation

File Structure

motion-subtitle.tsx
orbital-image-wheel.tsx

Usage

import { OrbitalImageWheel } from "@/components/unlumen-ui/orbital-image-wheel";

const images = [
  {
    src: "/photos/01.jpg",
    alt: "Capsule pack",
    label: "Capsule",
    subtitle: "Precision delivery",
  },
  {
    src: "/photos/02.jpg",
    alt: "Lab interior",
    label: "Lab",
    subtitle: "AI diagnostics",
  },
  {
    src: "/photos/03.jpg",
    alt: "Clinical scan",
    label: "Vision",
    subtitle: "Predictive screening",
  },
];

export default function Example() {
  return <OrbitalImageWheel images={images} />;
}

With custom subtitle animation controls

<OrbitalImageWheel
  images={images}
  subtitleDirection="bottom"
  subtitleSpeed={1.25}
  subtitleStagger={0.014}
/>

With a custom scroll container

If your layout scrolls inside a container (overflow-y-auto) instead of the viewport, pass scrollContainerRef so GSAP ScrollTrigger uses the correct scroller.

"use client";

import { useRef } from "react";
import { OrbitalImageWheel } from "@/components/unlumen-ui/orbital-image-wheel";

export default function ScrollContainerExample() {
  const scrollRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={scrollRef} className="h-[80vh] overflow-y-auto">
      <OrbitalImageWheel images={images} scrollContainerRef={scrollRef} />
    </div>
  );
}

Tuning wheel geometry and pacing

<OrbitalImageWheel
  images={images}
  turns={2.4}
  wheelSize={1800}
  cropRatio={0.7}
  itemWidth={260}
  itemHeight={340}
  scrollLength={360}
  focusSpread={0.28}
  scrollSensitivity={0.62}
/>

API Reference

PropTypeDefaultDescription
imagesOrbitalImageWheelImage[]-Array of image objects. Each item supports `src`, optional `alt`, optional `label`, and optional `subtitle`.
turns?number4Number of full wheel rotations during the scroll range.
blur?number4Maximum blur in pixels for out-of-focus cards.
dim?number40Minimum brightness percentage away from focus.
brightnessBoost?number30Extra brightness around the active focus card.
darknessStrength?number1.05Multiplier for out-of-focus darkening intensity.
minSaturation?number55Minimum saturation percentage away from focus.
saturationStrength?number0.6Multiplier for desaturation intensity away from focus.
focusSpread?number0.34Normalized angular width of the focus window.
scaleEffect?number0.06Scale reduction applied as cards move away from focus.
scrollSensitivity?number0.7Scroll sensitivity multiplier. Lower values require longer scrolling.
itemWidth?number220Card width in pixels.
itemHeight?number300Card height in pixels.
wheelSize?number-Optional fixed wheel diameter in pixels. When omitted, the wheel size is responsive.
cropRatio?number0.75How much of the wheel remains below the viewport. `0.5` keeps only the top half visible.
scrollLength?number330Section height in `vh`. Increase it to slow down the wheel progression.
captionOffset?number15Bottom offset of the centered caption block in viewport units.
showCaption?booleantrueShow or hide the centered active caption.
subtitleDirection?"top" | "bottom""top"Direction used by the subtitle character reveal animation.
subtitleSpeed?number1Speed multiplier for the subtitle animation (`> 1` is faster, `< 1` is slower).
subtitleStagger?number0.018Delay between subtitle character reveals, in seconds.
scrollContainerRef?RefObject<HTMLElement | null>-Optional ref to a custom scroll container. Use this for inner `overflow-y-auto` previews.
className?string-Additional classes applied to the root section.

How it works

The component combines GSAP ScrollTrigger and geometric transforms to simulate a cinematic wheel:

  1. A large circular layout is positioned below the viewport, so only its top arc is visible.
  2. Scroll progress rotates virtual card angles around the circle.
  3. Each card receives position, depth, tilt, blur, brightness, saturation, and scale based on its angular distance from the top focus anchor.
  4. The active index is stabilized with hysteresis to reduce jitter around boundaries.
  5. The caption updates from the active item and keeps the title pill centered with dynamic edge spacers.

Caption behavior

  • Subtitle source: image.subtitle -> image.alt -> "Visual Story"
  • Title source: image.label -> image.alt -> fallback "Image N"
  • Subtitle animation is delegated to MotionSubtitle via registry dependency (@unlumen-ui/motion-subtitle).

Tuning guide

  • Slower wheel motion: increase scrollLength and/or decrease scrollSensitivity.
  • Stronger foreground/background separation: increase blur, darknessStrength, saturationStrength, or brightnessBoost.
  • Softer effect: reduce blur, raise dim, and reduce scaleEffect.
  • More centered focus window: lower focusSpread for tighter spotlight, raise for broader spotlight.
  • More/less visible wheel arc: lower cropRatio to show more of the wheel, raise it to hide more below the fold.

Notes

  • The wheel center is anchored below the viewport so only its upper arc is visible.
  • Clicking a title pill animates scroll to the corresponding image position.
  • Captions are derived from the active image with built-in fallbacks.
  • Uses GSAP ScrollTrigger for wheel motion and Motion for subtitle text animation.
  • Works with viewport scroll and with custom scrollers through scrollContainerRef.

Credits

Built by leo.

Keep in mind

Most components on this site are inspired by or recreated from existing work across the web. I'm not here to take credit; just to learn, experiment, and sometimes push things a bit further. If something looks familiar and I forgot to mention you, reach out and I'll fix that right away.