悬停放大平移
悬停时图片放大,移动指针就能在图上平移,不用打开查看器也能看清细节。
用 React 和 CSS 制作悬停放大并平移的图片。外框设置 overflow hidden 和固定宽高比,内部图片填满外框。pointermove 时把图片的 transform-origin 设为指针所在的百分比位置;pointerenter 时用 300ms、cubic-bezier(0.16, 1, 0.3, 1) 的 transform 过渡把图片放大到 2 倍。transform-origin 不做过渡,或只给 80ms 的短过渡,让平移紧跟指针。pointerleave 时缩回 1 倍。使用 cursor: zoom-in。外框可聚焦,键盘用户在 :focus-visible 时以中心放大。在 prefers-reduced-motion 下放大不做过渡。说明文字为“沙丘细节,2 倍放大”。
源代码
'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>
)
}
.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; }
}