聚光卡片

card · spotlight · 灵感来源:Vercel Templates 的边框流光 + Linear 官网的内光斑(实测调研)

边框流光

glow="border"

外层跟随渐变,只露 1px 光边。

内光斑

glow="inner"

光斑在内容之下,7% accent 微光。

可点击

onClick + 双层全开

点我:0 次

复用卡

Border Glow + Inner Spotlight Card · 聚光卡片 · 卡片 / Spotlight

规格 · 术语提示词(给 AI 用)

做一个 Border Glow + Inner Spotlight Card(聚光卡片):
- 外层渐变边框跟随鼠标位置,白底盖住中心只露 1px 光边
- 内层 7% accent 微光斑压在内容之下
- 光层可单独开合
用这些 CSS 变量:--color-accent --color-surface --color-text --color-text-muted --dur-slow --fw-semibold --glass-border --mx --my --spot-on
灵感参考:Vercel Templates 的边框流光 + Linear 官网的内光斑(实测调研)

参数(CSS 变量)

--color-accent --color-surface --color-text --color-text-muted --dur-slow --fw-semibold --glass-border --mx --my --spot-on

必缺状态

(无必缺状态)

整页场景

(尚未接进整页场景)

安装(一条链接)

参考(本站可核对)

调研笔记

聚光卡片

调研数据

  • Linear 实测:独立 400×400 glow 层 + transform: translate(mouse-200) 跟随, 渐变中心恒在 50%;暗底白色 4% 微光,父层 overflow: hidden 裁剪
  • 边框流光三层路线对比:双层夹心(简单可靠)> mask 抠环(语法啰嗦)> 纯内光斑(最内敛)
  • 本组件双模式:glow="border" | "inner" | "both"

实现

  • 双层夹心:外层 p-px + 跟随渐变,内层白底盖住中心,露出 1px 光边
  • 鼠标位置写 CSS 变量 --mx/--my,光斑层 opacity: var(--spot-on) 淡入淡出
  • 浅色适配:Linear 的白微光在浅底不可见,光斑改 accent 紫(边框 60%、内部 6%)

关键参数

  • 光斑半径 220px;边框层渐变 accent → transparent 70%
  • 进出场 opacity 过渡 --dur-slow(300ms);内层圆角 = 外层圆角 - 1px,严丝合缝

实现

card/spotlight
import { useRef, type ReactNode, type MouseEvent } from 'react';

interface Props {
  title?: string;
  subtitle?: string;
  children?: ReactNode;
  onClick?: () => void;
  /** border = 边框流光,inner = 内光斑,both = 双层全开 */
  glow?: 'border' | 'inner' | 'both';
  className?: string;
}

export function SpotlightCard({
  title,
  subtitle,
  children,
  onClick,
  glow = 'both',
  className = '',
}: Props) {
  const ref = useRef<HTMLDivElement>(null);

  const setSpot = (on: boolean, e?: MouseEvent) => {
    const el = ref.current;
    if (!el) return;
    el.style.setProperty('--spot-on', on ? '1' : '0');
    if (on && e) {
      const r = el.getBoundingClientRect();
      el.style.setProperty('--mx', `${e.clientX - r.left}px`);
      el.style.setProperty('--my', `${e.clientY - r.top}px`);
    }
  };

  const hasBorder = glow === 'border' || glow === 'both';
  const hasInner = glow === 'inner' || glow === 'both';

  return (
    <div
      ref={ref}
      onMouseMove={(e) => setSpot(true, e)}
      onMouseLeave={() => setSpot(false)}
      onClick={onClick}
      role={onClick ? 'button' : undefined}
      tabIndex={onClick ? 0 : undefined}
      onKeyDown={onClick ? (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          onClick();
        }
      } : undefined}
      style={{ '--spot-on': '0', '--mx': '50%', '--my': '50%' } as React.CSSProperties}
      className={`
        group relative rounded-2xl p-px overflow-hidden
        bg-[var(--glass-border)]
        ${onClick ? 'cursor-pointer' : ''}
        ${className}
      `}
    >
      {/* 边框流光层:跟随鼠标的 accent 渐变,被内层盖住中心,只露 1px 光边 */}
      {hasBorder && (
        <div
          aria-hidden="true"
          className="absolute inset-0 rounded-2xl pointer-events-none transition-opacity duration-[var(--dur-slow)]"
          style={{
            opacity: 'var(--spot-on)',
            background:
              'radial-gradient(220px circle at var(--mx) var(--my), var(--color-accent), transparent 70%)',
          }}
        />
      )}

      {/* 内容层:白底盖住中心 */}
      <div className="relative rounded-[calc(1rem-1px)] bg-[var(--color-surface)] overflow-hidden">
        {/* 内光斑层:低透明度 accent,位于内容之下 */}
        {hasInner && (
          <div
            aria-hidden="true"
            className="absolute inset-0 pointer-events-none transition-opacity duration-[var(--dur-slow)]"
            style={{
              opacity: 'var(--spot-on)',
              background:
                'radial-gradient(260px circle at var(--mx) var(--my), color-mix(in srgb, var(--color-accent) 7%, transparent), transparent 70%)',
            }}
          />
        )}

        <div className="relative p-6">
          {(title || subtitle) && (
            <div className="mb-4">
              {title && (
                <h3 className="text-base" style={{ fontWeight: 'var(--fw-semibold)' }}>
                  {title}
                </h3>
              )}
              {subtitle && (
                <p className="text-sm text-[var(--color-text-muted)] mt-1">{subtitle}</p>
              )}
            </div>
          )}
          {children}
        </div>
      </div>
    </div>
  );
}

元信息

标识
card/spotlight
状态
done
最后提交
2026-11-01
风格轴
spotlight
依赖 token
--color-accent --color-surface --color-text --color-text-muted --dur-slow --fw-semibold --glass-border --mx --my --spot-on
在工作台打开 源码与 notes 都在 src/components/card/spotlight/