card · spotlight · 灵感来源:Vercel Templates 的边框流光 + Linear 官网的内光斑(实测调研)
glow="border"
外层跟随渐变,只露 1px 光边。
glow="inner"
光斑在内容之下,7% accent 微光。
onClick + 双层全开
点我:0 次
Border Glow + Inner Spotlight Card · 聚光卡片 · 卡片 / Spotlight
做一个 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 官网的内光斑(实测调研)--color-accent --color-surface --color-text --color-text-muted --dur-slow --fw-semibold --glass-border --mx --my --spot-on
(无必缺状态)
(尚未接进整页场景)
/r/card--spotlight.json
免外壳的 markdown 版:/llms/components/card--spotlight.mdtransform: translate(mouse-200) 跟随,
渐变中心恒在 50%;暗底白色 4% 微光,父层 overflow: hidden 裁剪glow="border" | "inner" | "both"p-px + 跟随渐变,内层白底盖住中心,露出 1px 光边--mx/--my,光斑层 opacity: var(--spot-on) 淡入淡出accent → transparent 70%--dur-slow(300ms);内层圆角 = 外层圆角 - 1px,严丝合缝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>
);
}
src/components/card/spotlight/