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
| Prop | Type | Default | Description |
|---|---|---|---|
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:
- A large circular layout is positioned below the viewport, so only its top arc is visible.
- Scroll progress rotates virtual card angles around the circle.
- Each card receives position, depth, tilt, blur, brightness, saturation, and scale based on its angular distance from the top focus anchor.
- The active index is stabilized with hysteresis to reduce jitter around boundaries.
- 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
MotionSubtitlevia registry dependency (@unlumen-ui/motion-subtitle).
Tuning guide
- Slower wheel motion:
increase
scrollLengthand/or decreasescrollSensitivity. - Stronger foreground/background separation:
increase
blur,darknessStrength,saturationStrength, orbrightnessBoost. - Softer effect:
reduce
blur, raisedim, and reducescaleEffect. - More centered focus window:
lower
focusSpreadfor tighter spotlight, raise for broader spotlight. - More/less visible wheel arc:
lower
cropRatioto 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.