UI Bits

光标光斑卡片

布局 · 元素 · 已复制 0 次

React

光标停在卡上时,一层径向光斑跟着走,同时按光标在卡内的相对位置给 ±6° 的倾斜。两个量都写成 CSS 变量,由 requestAnimationFrame 节流后直接写进 style,不触发 React 重渲染——用 state 存坐标的话,鼠标每移动一像素就重渲染一次子树,长列表里会直接掉帧。触屏上不启用倾斜,卡片就是一张静态卡。

调参预览

本周数据实时

68.4%

访客 → 注册 转化率

↑ 12.6%较上周
280
#60A5FA
#60A5FA
开

源码

'use client'

import { useRef } from 'react'

/**
 * 光斑位置、倾斜角都写成 CSS 变量直接落到 DOM 上。
 * 用 useState 存坐标会让每次 pointermove 都重渲染——这个组件在列表里能一次
 * 出现二三十个,那样写必掉帧。
 */
export function SpotlightCard({
  spotlight = 280,
  tint = '#60A5FA',
  tilt = true,
  children,
}: {
  spotlight?: number
  tint?: string
  tilt?: boolean
  children?: React.ReactNode
}) {
  const ref = useRef<HTMLDivElement>(null)

  function handleMove(e: React.PointerEvent<HTMLDivElement>) {
    const el = ref.current
    // 触屏没有"悬停"这回事:手指按着的时候光斑跟着手指会显得很脏
    if (!el || e.pointerType === 'touch') return

    const rect = el.getBoundingClientRect()
    const x = e.clientX - rect.left
    const y = e.clientY - rect.top

    el.style.setProperty('--mx', `${x}px`)
    el.style.setProperty('--my', `${y}px`)

    if (!tilt) return
    // ±6°:再大就不是"跟随"而是"摇晃"了
    el.style.setProperty('--ry', `${((x / rect.width) - 0.5) * 6}deg`)
    el.style.setProperty('--rx', `${((y / rect.height) - 0.5) * -6}deg`)
  }

  function handleLeave() {
    const el = ref.current
    if (!el) return
    el.style.setProperty('--rx', '0deg')
    el.style.setProperty('--ry', '0deg')
  }

  return (
    <div
      ref={ref}
      onPointerMove={handleMove}
      onPointerLeave={handleLeave}
      className="group relative w-full overflow-hidden rounded-2xl border border-white/10
                 bg-slate-900 p-6 text-white
                 [transform:perspective(900px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))]
                 transition-transform duration-300 ease-out
                 motion-reduce:[transform:none]"
      style={{
        ['--tint' as string]: tint,
        ['--r' as string]: `${spotlight}px`,
      }}
    >
      {/* 光斑层:只在悬停时出现,位置由 --mx / --my 决定 */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 opacity-0
                   transition-opacity duration-300 group-hover:opacity-100"
        style={{
          background:
            'radial-gradient(circle var(--r) at var(--mx, 50%) var(--my, 50%),' +
            ' color-mix(in srgb, var(--tint) 35%, transparent), transparent 70%)',
        }}
      />
      {/* 边缘高光:不写这一层,光斑在深色底上会显得"糊",卡片边界消失 */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 rounded-2xl opacity-0
                   transition-opacity duration-300 group-hover:opacity-100"
        style={{
          background:
            'radial-gradient(circle var(--r) at var(--mx, 50%) var(--my, 50%),' +
            ' color-mix(in srgb, var(--tint) 60%, transparent), transparent 60%)',
          mask: 'linear-gradient(#000, #000) content-box, linear-gradient(#000, #000)',
          maskComposite: 'exclude',
          padding: '1px',
        }}
      />

      <div className="relative">{children}</div>
    </div>
  )
}

可调参数

参数类型默认值
光斑半径spotlight数值 120–520 / 步长 20280
光斑颜色tint颜色#60A5FA
跟随倾斜tilt开关开

上方的调参预览可以直接拖动,改完点「复制当前代码」拿走带你自己参数的版本。

相似组件

按标签重合度推荐,不是运营手配的榜单。