GodUIGodUI
81

Anatomy of the Inertia Gallery

How a throwable gallery gets momentum, elasticity, and distance-based scale/opacity/blur from a single motion value, and why none of it triggers a React re-render while it's moving.

InertiaGallery is one draggable motion.div and one MotionValue. Every slide's scale, opacity, and blur are useTransform chains reading that same value — there's no per-slide state, no onDrag re-render loop, and no manual momentum math. Framer's own animation loop drives all of it.

One track, one motion value

The whole strip is a single flex row wrapped in a masked, overflow-hidden viewport. x is the one MotionValue everything downstream reads — dragging the track never touches React state:

Anatomymasked viewport, one flex track
Viewport
overflow-hidden, mask-image fade
Track
one flex row, drag="x"
Centered slide
distance-from-center is 0
tsx
 
const x = useMotionValue(-start * pitch);

<div className="overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]">
  <motion.div
    style={{ x, gap, paddingLeft: pad, paddingRight: pad }}
    drag="x"
    dragConstraints={{ left: -(count - 1) * pitch, right: 0 }}
    dragElastic={0.16}
    onDragEnd={handleDragEnd}
  />
</div>

The viewport pads each side by half the free space (measured with a ResizeObserver), which is what lets the first and last slide land dead center instead of pinned to an edge.

The throw

dragElastic: 0.16 lets the track travel a little past its constraints while you're still holding it — a small give, not a hard wall. Release, and onDragEnd either commits the nearest slide (commit) or, with snap, animates x to it on a spring:

tsx
 
const SETTLE_SPRING = { type: "spring", stiffness: 320, damping: 32, mass: 0.9 };

const handleDragEnd = () => {
  if (snap) goTo(nearest());
  else commit(nearest());
};
The throwelastic 0.16, settle 320 / 32 / 0.9
rest
thrown
overshoot
settle
0%
32%
55–74%
100%

Stiffness 320 with damping 32 is tuned to slightly underdamp — the strip overshoots the target by a few pixels before it settles, rather than arriving dead-on. mass: 0.9 keeps that overshoot small; it's felt, not seen.

The falloff

Every slide runs the same three-curve chain off its own distance from center, computed from the shared x value with useTransform — nothing here is per-slide state, it's a pure function re-evaluated on every frame x changes:

tsx
 
const nd = useTransform(x, (xv) =>
  Math.min(Math.abs(offset + xv) / pitch, 2.4),
);
const scale = useTransform(nd, (d) => 1 - d * 0.14 * falloff);
const opacity = useTransform(nd, (d) => clamp(1 - d * 0.3 * falloff, 0.4, 1));
const blurPx = useTransform(nd, (d) => (reduce ? 0 : Math.min(d * 3.2, 5) * falloff));
The falloffdistance → scale / opacity / blur
Scale
1 − distance × 0.14 × falloff
Opacity
clamped 0.4–1
Blur
0 when reduced-motion

Because scale, opacity, and filter are all compositor properties, Framer can update every slide's falloff every frame without asking the browser to lay anything out — the whole strip can be dragged at 60fps+ regardless of how many slides are off-screen.

Cheap when idle

Nothing here runs a requestAnimationFrame loop of its own. useTransform only recomputes when its input MotionValue changes, and x only changes while a drag or a settle animation is in flight — a gallery sitting still costs nothing beyond the one ResizeObserver watching the viewport.

Accessibility

The viewport is a labelled role="group" with aria-roledescription="carousel"; / on the viewport and the prev/next buttons both call the same goTo, so keyboard and pointer paths always land on the same index. useReducedMotion forces falloff to 0 — scale, opacity, and blur all flatten to their resting values, and the strip becomes a plain scrollable row with the spring/elastic feel switched off.

The result

Resultthe real component — throw the strip, or use the buttons
DuneDune
HarborHarbor
RidgeRidge
BloomBloom
QuartzQuartz
3 / 5

One motion value, three distance curves, one settle spring — throw the strip above, or use the buttons.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub