Photo Carousel

An image story focused on movement and pacing. Use arrow keys or click any card to move through the set.

Photo 1
Photo 2
Photo 3
Photo 4
Photo 5
Photo 6
Photo 7
Photo 8

Austin, you will always be my home


Making of

The first thing I saw when I signed up on X was a beautiful carousel made by @bossadizenith. As a photographer who loves going back to favorite shots, I had to recreate it.

Each photo has a different aspect ratio. The active card expands to its true proportions while previews stay a fixed size, so every card's position has to be recalculated from real widths rather than fixed slots.

// active card uses its native aspect ratio; previews are always 160×120
const getDimensions = (index) =>
  index !== activeIndex ? previewBase : getBaseDimensions(index)

// positions are computed from actual widths, not fixed slots
let x = getDimensions(activeIndex).width / 2 + gap
for (let d = 1; d < abs; d++) {
  const neighbor = activeIndex + d * sign
  x += getDimensions(neighbor).width + gap
}

The first version used spring physics, which felt too bouncy. Switching to a single cubic bezier applied to every property at once gave the motion a unified, deliberate feel.

const SMOOTH_EASE = [0.22, 1, 0.36, 1]

transition={{ type: 'tween', ease: SMOOTH_EASE, duration: 0.34 }}

Animating CSS filter causes unpredictable compositing across browsers. Instead, two image layers are stacked: a static grayscale base and a color layer that fades in with opacity only.

// static grayscale base
<Image style={{ filter: 'grayscale(100%)' }} />

// color layer fades in on active/hover - opacity only, no filter animation
<Image style={{ opacity: isActive || isHovered ? 1 : 0, transition: 'opacity 220ms ease-out' }} />