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

images
OrbitalImageWheelImage[]

Array of image objects. Each item supports `src`, optional `alt`, optional `label`, and optional `subtitle`.

turns?
number
4

Number of full wheel rotations during the scroll range.

blur?
number
4

Maximum blur in pixels for out-of-focus cards.

dim?
number
40

Minimum brightness percentage away from focus.

brightnessBoost?
number
30

Extra brightness around the active focus card.

darknessStrength?
number
1.05

Multiplier for out-of-focus darkening intensity.

minSaturation?
number
55

Minimum saturation percentage away from focus.

saturationStrength?
number
0.6

Multiplier for desaturation intensity away from focus.

focusSpread?
number
0.34

Normalized angular width of the focus window.

scaleEffect?
number
0.06

Scale reduction applied as cards move away from focus.

scrollSensitivity?
number
0.7

Scroll sensitivity multiplier. Lower values require longer scrolling.

itemWidth?
number
220

Card width in pixels.

itemHeight?
number
300

Card height in pixels.

wheelSize?
number

Optional fixed wheel diameter in pixels. When omitted, the wheel size is responsive.

cropRatio?
number
0.75

How much of the wheel remains below the viewport. `0.5` keeps only the top half visible.

scrollLength?
number
330

Section height in `vh`. Increase it to slow down the wheel progression.

captionOffset?
number
15

Bottom offset of the centered caption block in viewport units.

showCaption?
boolean
true

Show or hide the centered active caption.

subtitleDirection?
"top" | "bottom"
"top"

Direction used by the subtitle character reveal animation.

subtitleSpeed?
number
1

Speed multiplier for the subtitle animation (`> 1` is faster, `< 1` is slower).

subtitleStagger?
number
0.018

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