Ant Design BorderBeam 边框流光:实现鼠标悬浮时才显示的流光效果
Ant Design 自 6.4.0 起提供装饰性组件 BorderBeam(边框流光),用于在容器边框上渲染一段持续流动的渐变高亮。本文围绕官方示例 hover.tsx 讲解一个高频实战需求:默认隐藏边框流光,仅在鼠标 hover 到容器时才显示流光并让动画开始播放。读完本文,你将掌握该模式的完整代码、背后 animation-play-state / opacity 的 CSS 控制原理,以及 BorderBeam 流光层是如何通过 Portal、遮罩(mask)和 offset-path 动画实现的。
需求背景:hover 时显示的边框流光
对应的演示文档 hover.md 对效果的描述非常简洁:
默认隐藏边框流光,在鼠标 hover 到容器上时显示。
这适合登录面板、推荐卡片、重点 CTA 区域等场景——希望"平时低调、被关注时高亮"。组件文档 index.zh-CN.md 在"何时使用"中也给出定位:BorderBeam 用于强化容器的视觉关注度,但不引入业务状态语义,不应替代焦点态、校验态或业务状态边框。
BorderBeam 的完整 API 参数如下(摘自 index.zh-CN.md):
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|---|---|---|---|---|
| children | 装饰内容 | ReactNode |
- | 6.4.0 |
| color | 流光颜色配置,支持单色字符串或渐变停靠点数组,percent 使用 0 ~ 100 区间,组件内部为尾部透明过渡预留空间 |
string | { color: string; percent: number }[] |
- | 6.4.0 |
| count | 流光数量 | number |
1 | 6.6.0 |
| duration | 流光完成一圈动画的时间(秒) | number |
6 | 6.5.0 |
| lineWidth | 流光线宽,数字按像素处理 | number | string |
1px |
6.5.0 |
| outset | 流光层相对容器边缘的外扩距离,遇到裁剪容器时可设为 0 |
number | string |
- | 6.4.0 |
| size | 流光可见段的尺寸,数字按像素处理 | number | string |
100 | 6.5.0 |
完整实现代码
官方示例 hover.tsx 的完整实现如下:
import React from 'react';
import { BorderBeam, Card } from 'antd';
import { createStyles } from 'antd-style';
const useStyles = createStyles((props) => {
const { css, prefixCls, cssVar } = props;
return {
card: css`
width: 360px;
.${prefixCls}-border-beam {
opacity: 0;
transition: opacity ${cssVar.motionDurationMid};
&::before {
animation-play-state: paused;
}
}
&:hover {
.${prefixCls}-border-beam {
opacity: 1;
&::before {
animation-play-state: running;
}
}
}
`,
};
});
const Demo: React.FC = () => {
const { styles } = useStyles();
return (
<BorderBeam>
<Card className={styles.card} title="Hover over the card">
The border beam appears when the pointer moves over this card.
</Card>
</BorderBeam>
);
};
export default Demo;
这段代码的关键点有三个:
- 选择器锚定
.${prefixCls}-border-beam:prefixCls来自 antd 的 ConfigProvider,默认前缀为ant,因此实际命中的类名是.ant-border-beam。该元素正是BorderBeam插入到被装饰容器内部(通过 Portal)的流光层,而不是被装饰的容器本身。 opacity: 0 → 1+transition: opacity ${cssVar.motionDurationMid}:控制整个流光层的淡入淡出,过渡时长取的是 antd 主题 CSS 变量motionDurationMid,与全局动效时长保持一致。&::before { animation-play-state: paused / running }:流光本体是流光层的::before伪元素,hover 时把动画从暂停切换为运行,离开时再暂停。这样不仅隐藏了效果,也避免了不可见时的无效动画开销。
源码剖析:为什么这个 CSS 方案可行
要理解上面这段 CSS 为什么恰好是"最少必要"的控制面,需要看 BorderBeam 的内部结构。
流光层是 Portal 插入的子节点
从 BorderBeam.tsx 的渲染逻辑看,组件通过 useChildDom 从 children 中解析出真实的 DOM 节点(要求 children 是原生元素或正确透传 ref 的组件),然后为每份流光渲染一个 BorderBeamEffect:
// components/border-beam/BorderBeamEffect.tsx
if (!hostDom || !isHTMLElement(hostDom)) {
return null;
}
return createPortal(<BorderBeamEffectElement prefixCls={prefixCls} {...rest} />, hostDom);
也就是说,.ant-border-beam 这个 div 被 Portal 直接插进 children 对应的容器 DOM 内部,并且带 aria-hidden="true"。这正是 hover 示例中用 .card .ant-border-beam 后代选择器能够精准命中流光层的原因——它在 DOM 树上是卡片的后代。
流光本体是 ::before + offset-path 动画
样式定义在 style/index.ts,核心结构是:
- 流光层(
.ant-border-beam):position: absolute、inset取 CSS 变量--border-beam-inset-offset(默认与子容器实际边框宽度取负值对齐)、border-radius: inherit、overflow: hidden、pointer-events: none,并用padding: var(--border-beam-line-width, 1px)撑出边框带宽; - 遮罩:通过
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)加mask-composite: exclude(以及-webkit-mask-composite: xor)裁掉中心区域,只保留边缘一圈,形成"只有边框可见"的环形通道; - 流光本体
&::before:一个边长为var(--border-beam-size, 100px)的正方形渐变层(background-image取var(--border-beam-beam-gradient)),沿offset-path: rect(0 auto auto 0 round var(--border-beam-size))路径运动,动画为antBorderBeamMove(offset-distance从0%到100%),时长var(--border-beam-duration, 6s),线性、无限循环。
// components/border-beam/style/index.ts(节选)
'&::before': {
...genNoMotionRawStyle(),
content: '""',
position: 'absolute',
width: varRef('size', '100px'),
aspectRatio: '1 / 1',
opacity: 0.95,
backgroundImage: varRef('beam-gradient', defaultBeamGradient),
offsetPath: `rect(0 auto auto 0 round ${varRef('size', '100px')})`,
offsetRotate: 'auto',
animationName: antBorderBeamMove,
animationDuration: varRef('duration', `${DEFAULT_BORDER_BEAM_DURATION}s`),
animationTimingFunction: 'linear',
animationIterationCount: 'infinite',
willChange: 'offset-distance',
},
把这两层结构对照 hover 示例:控制外层 div 的 opacity 就控制了整条流光层的显隐;控制 ::before 的 animation-play-state 就控制了那条沿路径奔跑的渐变是否真的在跑。两者配合,就是官方示例的完整方案。
另外值得注意,样式中有 @supports 双重降级:只有浏览器同时支持 mask-composite 与 offset-path: rect(...) 时,流光层才会 display: block,否则整个组件保持 display: none 静默降级,不会渲染半成品效果。
渐变的尾部透明区与 percent 语义
如果给 color 传入渐变停靠点数组,util.ts 会把用户输入的 0 ~ 100 停靠点缩放到前 70%(MAX_BEAM_COLOR_STOP_PERCENT = 70)区间内:
// components/border-beam/util.ts(节选)
export const DEFAULT_BORDER_BEAM_DURATION = 6;
export const MAX_BEAM_COLOR_STOP_PERCENT = 70;
const getMappedBeamColorStopPercent = (percent: number) =>
Number(((Math.min(Math.max(percent, 0), 100) / 100) * MAX_BEAM_COLOR_STOP_PERCENT).toFixed(2));
组件保留后 30% 作为透明过渡区,保证流光尾部渐隐、尾迹连续可见。默认渐变(未传 color 时)由主题色派生:
const defaultBeamGradient = `linear-gradient(to left, ${colorPrimary} 0%, ${colorPrimaryHover} 70%, transparent)`;
因此 hover 示例不传 color 时,流光颜色会自动跟随主题的主色(colorPrimary / colorPrimaryHover)。
多条流光与 CSS 变量注入
BorderBeam.tsx 中,count、duration、lineWidth、size 等属性并不直接生成样式,而是写成 CSS 变量注入到流光层的 style 上:
...(beamGradient && { [varName('beam-gradient')]: beamGradient }),
...(isNumber(duration) && duration > 0 && { [varName('duration')]: `${duration}s` }),
...(isNonNullable(lineWidth) && { [varName('line-width')]: unit(lineWidth) }),
...(isNonNullable(size) && { [varName('size')]: unit(size) }),
...(index > 0 && {
[varName('delay')]: `${(-mergedDuration * index) / mergedCount}s`,
}),
其中 count > 1 时第 i 条流光会获得负的 animation-delay(-duration * i / count),让多条流光在路径上均匀错开。这也意味着:hover 方案中对 ::before 的 animation-play-state 控制天然对多条流光同时生效,无需逐条处理。
使用前提与注意事项
结合组件文档 index.zh-CN.md 的 FAQ 和源码结构,hover 方案落地时有几点需要注意:
children必须能解析出真实 DOM 节点。组件需要把流光层 Portal 进子节点内部,因此被包裹内容应是原生 DOM 元素或正确透传ref的 React 组件;children是否可以插入会在初始化时判断,之后不会持续监听结构变化。示例中使用Card,正是因为它把ref透传到了内部元素。- 被装饰节点需要定位上下文。流光层使用
position: absolute定位并依赖border-radius: inherit,通常应给容器设置position: relative(示例中Card自身样式已提供)。 animation-play-state不会重置进度。hover 离开时动画暂停在当前位置,再次进入时从暂停处继续,而不是从头开始。若希望每次 hover 都从起点播,可以把"暂停"改为把display切为none或用状态控制key强制重建——不过对装饰性流光而言,"接续播放"通常视觉上更自然。- 尊重系统动效偏好。样式中已包含
@media (prefers-reduced-motion: reduce) { &::before { display: none } },命中"减少动态效果"系统设置时流光本体直接不渲染,hover 方案无需额外处理。 size的取值限制。流光由边长为size的方形渐变层生成,经过水平边框时向两侧各延伸约size / 2,若size接近遮罩覆盖层短边的两倍,可能同时覆盖上下边框。建议满足size < 2 × min(width, height),示例中 360px 宽的Card使用默认size: 100是安全的。
小结
Ant Design BorderBeam 的 hover 示例给出了一套非常克制的"按需显示流光"方案:利用组件把流光层 Portal 进被装饰容器、流光本体是 ::before 伪元素这两个内部结构事实,仅用 opacity + transition 控制显隐、用 animation-play-state 控制播放状态,即可在不改动任何 React 状态的前提下,实现鼠标悬浮时才亮起并奔跑的边框流光效果。若还需要渐变色、多条流光、自定义时长等能力,可继续参考 customized-color.tsx、count.tsx、duration.tsx 等官方演示。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00