首页
/ Ant Design BorderBeam 边框流光:实现鼠标悬浮时才显示的流光效果

Ant Design BorderBeam 边框流光:实现鼠标悬浮时才显示的流光效果

2026-09-06 15:03:13作者:邬祺芯Juliet

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;

这段代码的关键点有三个:

  1. 选择器锚定 .${prefixCls}-border-beamprefixCls 来自 antd 的 ConfigProvider,默认前缀为 ant,因此实际命中的类名是 .ant-border-beam。该元素正是 BorderBeam 插入到被装饰容器内部(通过 Portal)的流光层,而不是被装饰的容器本身。
  2. opacity: 0 → 1 + transition: opacity ${cssVar.motionDurationMid}:控制整个流光层的淡入淡出,过渡时长取的是 antd 主题 CSS 变量 motionDurationMid,与全局动效时长保持一致。
  3. &::before { animation-play-state: paused / running }:流光本体是流光层的 ::before 伪元素,hover 时把动画从暂停切换为运行,离开时再暂停。这样不仅隐藏了效果,也避免了不可见时的无效动画开销。

源码剖析:为什么这个 CSS 方案可行

要理解上面这段 CSS 为什么恰好是"最少必要"的控制面,需要看 BorderBeam 的内部结构。

流光层是 Portal 插入的子节点

BorderBeam.tsx 的渲染逻辑看,组件通过 useChildDomchildren 中解析出真实的 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: absoluteinset 取 CSS 变量 --border-beam-inset-offset(默认与子容器实际边框宽度取负值对齐)、border-radius: inheritoverflow: hiddenpointer-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-imagevar(--border-beam-beam-gradient)),沿 offset-path: rect(0 auto auto 0 round var(--border-beam-size)) 路径运动,动画为 antBorderBeamMoveoffset-distance0%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 示例:控制外层 divopacity 就控制了整条流光层的显隐;控制 ::beforeanimation-play-state 就控制了那条沿路径奔跑的渐变是否真的在跑。两者配合,就是官方示例的完整方案。

另外值得注意,样式中有 @supports 双重降级:只有浏览器同时支持 mask-compositeoffset-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 中,countdurationlineWidthsize 等属性并不直接生成样式,而是写成 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 方案中对 ::beforeanimation-play-state 控制天然对多条流光同时生效,无需逐条处理。

使用前提与注意事项

结合组件文档 index.zh-CN.md 的 FAQ 和源码结构,hover 方案落地时有几点需要注意:

  1. children 必须能解析出真实 DOM 节点。组件需要把流光层 Portal 进子节点内部,因此被包裹内容应是原生 DOM 元素或正确透传 ref 的 React 组件;children 是否可以插入会在初始化时判断,之后不会持续监听结构变化。示例中使用 Card,正是因为它把 ref 透传到了内部元素。
  2. 被装饰节点需要定位上下文。流光层使用 position: absolute 定位并依赖 border-radius: inherit,通常应给容器设置 position: relative(示例中 Card 自身样式已提供)。
  3. animation-play-state 不会重置进度。hover 离开时动画暂停在当前位置,再次进入时从暂停处继续,而不是从头开始。若希望每次 hover 都从起点播,可以把"暂停"改为把 display 切为 none 或用状态控制 key 强制重建——不过对装饰性流光而言,"接续播放"通常视觉上更自然。
  4. 尊重系统动效偏好。样式中已包含 @media (prefers-reduced-motion: reduce) { &::before { display: none } },命中"减少动态效果"系统设置时流光本体直接不渲染,hover 方案无需额外处理。
  5. 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.tsxcount.tsxduration.tsx 等官方演示。

登录后查看全文
热门项目推荐
相关项目推荐