组件
透视倾斜 Tilt Card
卡片跟随光标指针在三维空间中产生细腻的 3D 透视倾斜与反光眩光,并基于物理弹簧平滑回正。
基础用法
悬停在作品卡片上,卡片随指针产生 3D 倾斜并淡入高光,底部信息栏通过 translateZ 浮在表面之上:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/tilt-card安装动效依赖与工具库
pnpm add motion clsx tailwind-merge复制组件源码到
components/ui/tilt-card.tsx"use client"
import * as React from "react"
import {
motion,
useMotionTemplate,
useMotionValue,
useReducedMotion,
useSpring,
type HTMLMotionProps,
} from "motion/react"
import { cn } from "@/lib/utils"
export interface TiltCardProps extends Omit<
HTMLMotionProps<"div">,
"children"
> {
/** Content rendered on the tilting plane. */
children: React.ReactNode
/** Maximum rotation on either axis in degrees. @default 9 */
maxTilt?: number
/** Perspective depth in pixels. @default 900 */
perspective?: number
/** Scale applied while the pointer is over the card. @default 1.015 */
hoverScale?: number
/** Show a pointer-position glare layer that fades in on hover. @default false */
glare?: boolean
/** Peak opacity of the glare layer. @default 0.35 */
glareOpacity?: number
/** Classes applied to the optional glare layer. */
glareClassName?: string
}
const tiltSpring = { stiffness: 220, damping: 24, mass: 0.45 }
/** Tilts a surface in 3D according to the pointer position and springs to rest. */
function TiltCard({
children,
maxTilt = 9,
perspective = 900,
hoverScale = 1.015,
glare = false,
glareOpacity = 0.35,
className,
glareClassName,
style,
onPointerEnter,
onPointerMove,
onPointerLeave,
...props
}: TiltCardProps) {
const reduceMotion = useReducedMotion()
// Measured at rest on enter; reading the rect of a tilted card makes the input feed back into itself.
const rectRef = React.useRef<DOMRect | null>(null)
const rotateXValue = useMotionValue(0)
const rotateYValue = useMotionValue(0)
const scaleValue = useMotionValue(1)
const glareVisibility = useMotionValue(0)
const glareX = useMotionValue(50)
const glareY = useMotionValue(50)
const rotateX = useSpring(rotateXValue, tiltSpring)
const rotateY = useSpring(rotateYValue, tiltSpring)
const scale = useSpring(scaleValue, { stiffness: 300, damping: 26 })
const glareAlpha = useSpring(glareVisibility, { stiffness: 160, damping: 26 })
const glareBackground = useMotionTemplate`radial-gradient(circle at ${glareX}% ${glareY}%, white, transparent 52%)`
function handleMove(event: React.PointerEvent<HTMLDivElement>) {
const rect = rectRef.current ?? event.currentTarget.getBoundingClientRect()
const x = Math.min(Math.max((event.clientX - rect.left) / rect.width, 0), 1)
const y = Math.min(Math.max((event.clientY - rect.top) / rect.height, 0), 1)
rotateXValue.set((0.5 - y) * maxTilt * 2)
rotateYValue.set((x - 0.5) * maxTilt * 2)
glareX.set(x * 100)
glareY.set(y * 100)
}
return (
<motion.div
data-slot="tilt-card"
className={cn("relative transform-gpu", className)}
style={{
...style,
rotateX: reduceMotion ? 0 : rotateX,
rotateY: reduceMotion ? 0 : rotateY,
scale: reduceMotion ? 1 : scale,
transformPerspective: perspective,
transformStyle: "preserve-3d",
}}
onPointerEnter={(event) => {
if (!reduceMotion && event.pointerType !== "touch") {
rectRef.current = event.currentTarget.getBoundingClientRect()
scaleValue.set(hoverScale)
glareVisibility.set(glareOpacity)
handleMove(event)
}
onPointerEnter?.(event)
}}
onPointerMove={(event) => {
if (!reduceMotion && event.pointerType !== "touch") handleMove(event)
onPointerMove?.(event)
}}
onPointerLeave={(event) => {
rectRef.current = null
rotateXValue.set(0)
rotateYValue.set(0)
scaleValue.set(1)
glareVisibility.set(0)
onPointerLeave?.(event)
}}
{...props}
>
{children}
{glare && !reduceMotion ? (
<motion.div
aria-hidden="true"
data-slot="tilt-card-glare"
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit] mix-blend-soft-light",
glareClassName
)}
style={{ background: glareBackground, opacity: glareAlpha }}
/>
) : null}
</motion.div>
)
}
export { TiltCard }
属性 Props
TiltCard 接受以下 3D 动效与光效配置属性,并继承 motion.div 的全部 HTML 属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| children | React.ReactNode | — | 卡片内部渲染的内容,可结合 CSS translateZ 建立多维纵深分层。 |
| maxTilt | number | 9 | 卡片在 X / Y 轴方向上的最大倾斜旋转角度(度 deg)。建议保持在 6~12 度之间。 |
| perspective | number | 900 | 三维透视距离(像素 px)。数值越大透视越平缓,数值越小空间畸变感越强。 |
| hoverScale | number | 1.015 | 光标悬停在卡片上方时的微缩放倍率。 |
| glare | boolean | false | 是否开启随光标坐标移动的表面高光图层。高光仅在悬停时淡入。 |
| glareOpacity | number | 0.35 | 悬停时高光层淡入到的峰值不透明度;指针离开时随弹簧淡出。 |
| glareClassName | string | — | 应用于高光图层的额外 CSS 类名(如 mix-blend 混合模式)。不透明度请使用 `glareOpacity`。 |
| className | string | — | 应用于外层卡片容器的额外 CSS 类名。 |
事件 Events
TiltCard 继承 Motion 容器的事件回调:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onPointerMove | (event: React.PointerEvent<HTMLDivElement>) => void | — | 指针在卡片内移动时触发,内部通过 MotionValue 计算 X/Y 倾角并触发物理弹簧插值。 |
| onPointerLeave | (event: React.PointerEvent<HTMLDivElement>) => void | — | 指针离开卡片时触发,内部自动通过 Spring 回弹归零复位。 |
使用场景与设计规范
TiltCard 为界面带来触手可及的“实体感”与空间交互魅力。
- 使用场景:
- 数字会员卡 / 凭证(Pass & Badges):极具科技感的开发者通行证、VIP 权益卡。
- 核心作品 / 摄影与艺术案例:作品集封面、获奖项目展示。
- 旗舰硬件 / 核心特性展示:让用户在鼠标滑动中全方位感受产品的精细质感。
- 3D 分层与纵深技巧:
- 卡片内部容器默认启用了
transform-style: preserve-3d。 - 在内部子元素上添加 Tailwind 类名
[transform:translateZ(24px)]或[transform:translateZ(36px)],可以使文字和图标“悬浮”在卡片表面上方,产生视差立体感。 - 注意:
overflow-hidden会强制扁平化 3D 上下文,使translateZ失效。需要裁切图片时,请把overflow-hidden放在图片自身或内层容器上,而不是卡片根节点。 - 指针位置基于进入时测得的静止尺寸计算,避免倾斜后的包围盒反过来影响角度,边缘处不会抖动。
- 卡片内部容器默认启用了
- 克制设计原则:
- 避免在密集的数据表格行或长列表项中过度使用 3D 倾斜,以免干扰用户的浏览与点击精度。
场景示例
会员卡
三层内容分别使用不同的 translateZ 深度,配合高光打造会员卡的实体感:
Loading…
无障碍与交互 Accessibility
- 自动无障碍降级:内置
useReducedMotion监听。当系统开启了“减少动态效果”时,3D 旋转与缩放动效自动归零(Static 0deg / 1.0 Scale),高光层不渲染,避免任何视觉眩晕。 - 触摸屏保护:在移动端触摸(Touch)环境下自动跳过倾斜计算,避免与页面上下滑动手势冲突。
- 反光层穿透:高光层标记为
aria-hidden="true"且带有pointer-events-none,不遮挡内部可聚焦元素。