Hover Zoom & Pan

Hovering zooms the image in, and moving the pointer pans across it, so shoppers can inspect detail without opening a viewer.

  • CSS
  • Images
  • Pointer
  • Added
Desert dunes, 2× detail

AI prompt

Create a hover zoom-and-pan image in React and CSS. The frame has overflow hidden and a fixed aspect ratio; the image inside fills it. On pointermove, set transform-origin on the image to the pointer position in percent, and on pointerenter scale the image to 2 with a 300ms cubic-bezier(0.16, 1, 0.3, 1) transform transition. Keep transform-origin untransitioned, or give it only a short 80ms transition, so panning follows the pointer closely. Scale back to 1 on pointerleave. Use cursor: zoom-in. Make the frame focusable and zoom to the centre on :focus-visible for keyboard users. Under prefers-reduced-motion, zoom without the transition. The caption reads "Desert dunes, 2× detail".

Source

Demo.tsx
'use client'

import s from './demo.module.css'

export default function HoverZoomPan({ labels }: { labels: Record<string, string> }) {
  const onMove = (e: React.PointerEvent<HTMLElement>) => {
    const r = e.currentTarget.getBoundingClientRect()
    const scene = e.currentTarget.firstElementChild as HTMLElement
    scene.style.transformOrigin = `${((e.clientX - r.left) / r.width) * 100}% ${((e.clientY - r.top) / r.height) * 100}%`
  }

  return (
    <figure className={s.frame} tabIndex={0} onPointerMove={onMove}>
      <div className={s.scene} role="img" aria-label={labels.caption} />
      <figcaption className={s.caption}>{labels.caption}</figcaption>
    </figure>
  )
}
demo.module.css
.frame {
  --sky: oklch(80% 0.07 70);
  --dune-1: oklch(68% 0.13 55);
  --dune-2: oklch(56% 0.13 45);
  --dune-3: oklch(44% 0.11 40);
  --sun: oklch(95% 0.05 85);
  position: relative;
  width: min(300px, 100%);
  aspect-ratio: 16 / 10;
  margin: 0;
  overflow: hidden;
  border-radius: 14px;
  cursor: zoom-in;
}

.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

.scene {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 60% 30% at 70% 100%, var(--dune-3) 98%, transparent 100%),
    radial-gradient(ellipse 70% 34% at 20% 92%, var(--dune-2) 98%, transparent 100%),
    radial-gradient(ellipse 80% 40% at 60% 80%, var(--dune-1) 98%, transparent 100%),
    repeating-linear-gradient(170deg, transparent 0 6px, oklch(100% 0 0 / 0.06) 6px 7px),
    radial-gradient(circle at 26% 30%, var(--sun) 0 7%, transparent 7.5%),
    linear-gradient(var(--sky), var(--dune-1));
  transition: transform 300ms var(--ease-out), transform-origin 80ms linear;
}

.frame:hover .scene { transform: scale(2); }
.frame:focus-visible .scene { transform: scale(2); transform-origin: center; }

.caption {
  position: absolute;
  left: 10px;
  bottom: 8px;
  padding: 3px 8px;
  border-radius: 6px;
  background: oklch(0% 0 0 / 0.45);
  color: var(--stage-ink);
  font: 500 var(--text-xs) / 1.3 var(--font-body);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .scene { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.