首页
/ antd BorderBeam size 属性详解:控制流光可见段尺寸的实现原理与调优实践

antd BorderBeam size 属性详解:控制流光可见段尺寸的实现原理与调优实践

2026-09-06 15:08:38作者:裘旻烁

本文围绕 Ant Design(antd)BorderBeam 组件的 size 属性展开:从官方示例 size 演示 出发,讲清 size 控制“流光可见段尺寸”的确切含义、默认值 100px 的由来、数字与字符串两类取值的处理规则,并结合 组件源码单元测试 说明该属性如何经由 CSS 变量驱动底层的 offset-path 动画,以及 size 取值过大时会产生“上下边框同时点亮”的几何边界条件。读完后你可以为登录面板、推荐卡片等容器精确调节流光段的长短,并避开 size 超限导致的视觉瑕疵。

1. size 是什么:可见光束段的边长

BorderBeam 是 antd 6.4.0 引入的装饰性组件,用于给容器边框添加一段沿边框持续流动的高亮(“流光”效果)。在 中文文档 的定位中,它适合登录面板、推荐卡片、AI 模块、重点 CTA 区域等需要强化视觉关注的场景,但不应替代焦点态、校验态或业务状态边框。

完整属性列表如下(摘自 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

其中 size 从 6.5.0 开始提供,核心语义是:设置流光可见段的尺寸,数字类型按像素处理,默认值为 100px。要理解这一点,先看它生成的 DOM 结构——流光由一个边长为 size 的方形渐变层生成,该渐变层沿容器边框移动,遮罩(mask)只显示它与边框重叠的区域。也就是说:

  • size 设定的是这个方形渐变层的边长,而不是流光沿边框路径移动的距离;
  • 它不随边框周长缩放,因此小卡片和大横幅上 size=100 的流光段长度是相同的,视觉效果密度不同。

2. 官方示例:三档尺寸对比

components/border-beam/demo/size.tsx 用一个 2 列网格对比了三档 size 取值,这是理解该属性最直观的起点:

import React from 'react';
import { BorderBeam, Card, Tag, Typography } from 'antd';

const sizes: Array<{
  name: string;
  size?: number | string;
  bodyMinHeight: number;
  description: string;
  spanFull?: boolean;
}> = [
  {
    name: 'Default',
    bodyMinHeight: 112,
    description: 'Uses the default 100px visible beam segment.',
  },
  {
    name: 'Compact',
    size: 56,
    bodyMinHeight: 112,
    description: 'Keeps the highlight shorter for dense card groups.',
  },
  {
    name: 'Extended',
    size: 160,
    bodyMinHeight: 192,
    description: 'Creates a longer highlight for wider feature panels.',
    spanFull: true,
  },
];

const App: React.FC = () => (
  <div
    style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
      gap: 32,
      maxWidth: 960,
    }}
  >
    {sizes.map(({ name, size, bodyMinHeight, description, spanFull }) => (
      <div key={name} style={{ gridColumn: spanFull ? '1 / -1' : undefined }}>
        <BorderBeam size={size}>
          <Card
            title={name}
            extra={<Tag variant="filled">{size ?? 100}px</Tag>}
            styles={{ body: { minHeight: bodyMinHeight, display: 'flex', alignItems: 'center' } }}
          >
            <Typography.Text type="secondary">{description}</Typography.Text>
          </Card>
        </BorderBeam>
      </div>
    ))}
  </div>
);

export default App;

示例中的三个取值各有明确的适用场景,可归纳为一条选型经验:

  • 不传 size(默认 100pxsizes 数组第一项 sizeundefined,依赖默认值。适合最常见的卡片场景,是官方建议的基准值。
  • size={56}(Compact):更短的可见段,适合“密集卡片组”——例如列表里并排的多张小卡片。流光段短小,不会在小容器里显得拖沓。
  • size={160}(Extended):更长的可见段,适合“更宽的特性面板”。示例中它独占整行(spanFull: truegridColumn: '1 / -1'),因为更长的流光需要更大的容器来承载。

注意示例中 size 的类型是 number | string 联合类型:size 支持传入 CSS 尺寸字符串(如 '12em'),数字则按像素处理,这一点在后面的测试用例中有直接验证。

3. 源码链路:size 如何变成一段动画

size 的传递路径可以完整追踪到 CSS 变量,整条链路在源码中非常清晰:

第一步:属性解构与透传。BorderBeam.tsx 中,size 作为可选属性参与渲染,仅在非空时写入 CSS 变量:

const { ..., size } = props;
// ...
return (
  <>
    {childNode}
    {Array.from({ length: mergedCount }, (_, index) => (
      <BorderBeamEffect
        key={index}
        ...
        style={{
          ...
          ...(isNonNullable(size) && { [varName('size')]: unit(size) }),
          ...
        }}
      />
    ))}
  </>
);

这里 unit 来自 @ant-design/cssinjs:数字会被补上 px 单位(160160px),字符串原样透传('12em'12em)。CSS 变量名通过 genCssVar(getPrefixCls(), 'border-beam') 生成,最终落在宿主元素的行内样式上。

第二步:流光层注入。 BorderBeamEffect.tsx 通过 createPortal.ant-border-beam 节点插入到子组件解析出的真实 DOM 节点内部。若 hostDom 不存在或不是 HTMLElement,则直接返回 null(不渲染流光层)。

第三步:CSS 消费 size 变量。style/index.ts 中,::before 伪元素就是那个“方形渐变层”,size 变量同时控制它的边长和运动轨迹:

'@supports (offset-path: rect(0 auto auto 0 round 1px))': {
  display: 'block',

  '&::before': {
    ...genNoMotionRawStyle(),
    content: '""',
    position: 'absolute',
    top: 0,
    left: 0,
    width: varRef('size', '100px'),        // 渐变层边长,默认 100px
    aspectRatio: '1 / 1',                   // 强制正方形
    opacity: 0.95,
    backgroundImage: varRef('beam-gradient', defaultBeamGradient),
    offsetAnchor: '90% 50%',
    offsetDistance: '0%',
    offsetPath: `rect(0 auto auto 0 round ${varRef('size', '100px')})`, // 沿边框的圆角矩形路径
    offsetRotate: 'auto',
    animationName: antBorderBeamMove,      // offsetDistance: 0% -> 100%
    animationDuration: varRef('duration', `${DEFAULT_BORDER_BEAM_DURATION}s`),
    ...
  },
},

三个关键点:

  1. width: varRef('size', '100px') 配合 aspectRatio: '1 / 1',方形渐变层的边长就是 size,默认回退值 100px 与此处对应;
  2. offsetPath: rect(... round size) 让该层的锚点沿着容器边框的圆角矩形路径运动,动画本身只是 offsetDistance0%100% 的线性循环;
  3. 父层 .ant-border-beam 使用 mask-composite: exclude(或 WebKit 的 -webkit-mask-composite: xor)做边框遮罩,只显示渐变层与边框重叠的窄条区域——这就是“可见段”的由来:size 越宽,同一时刻穿过遮罩区域被显示出来的部分就越长。

此外该样式块受双重 @supports 保护(mask-compositeoffset-path: rect()),并且包含 prefers-reduced-motion: reduce 媒体查询:开启“减少动态效果”时 ::before 直接隐藏,即文档 FAQ 中说明的“组件会隐藏 beam 效果”。

4. size 的取值限制:size < 2 × min(width, height)

这是 size 调优中最重要的约束,index.zh-CN.md 的 FAQ「size 的取值限制」给出了完整推导:

流光由一个边长为 size 的方形渐变层生成。渐变层沿容器边框移动,遮罩只显示它与边框重叠的区域。size 设置的是渐变层边长,不按边框路径长度计算。

流光经过水平边框时,方形渐变层会向边框两侧各延伸约 size / 2。当 size 接近或超过遮罩覆盖层高度的两倍,它可能同时覆盖上下边框。流光经过垂直边框时,宽度方向同理。

使用时应让 size 明显小于遮罩覆盖层短边的两倍:size < 2 × min(width, height)。遮罩覆盖层通常与被装饰容器大小接近,outset 会改变其尺寸。圆角、lineWidth 和渐变透明区域也会影响重叠开始可见的位置。

用官方示例的三档值代入验证这条约束:

场景 容器短边(近似) size 2 × 短边 是否安全
Compact 卡片 约 112px body 高 + 头部 ≈ 160px+ 56px 320px+ 安全
默认卡片 同上 100px 320px+ 安全
Extended 面板 更宽更高的全宽面板 160px 更大 安全

反面教材则是:如果一个只有 120px 高的窄卡片用了 size={300},当流光扫过上下边框时,方形渐变层(300px 高)会同时与上、下两条边框重叠,视觉上上下边框会一起被点亮,流光的“流动感”消失。

两个会改变“遮罩覆盖层尺寸”的相关因素:

  • outset:流光层相对容器边缘的外扩距离,会改变遮罩覆盖层的实际尺寸,外扩时约束条件中的 min(width, height) 相应增大;
  • 圆角 / lineWidth / 渐变透明区:都会影响重叠“开始可见”的具体位置,属于二阶修正,通常无需精确计算,但调小容器时要注意。

5. 测试用例:数字与字符串取值的实证

components/border-beam/tests/index.test.tsx 中的 should support customizing the beam size 用例完整覆盖了 size 的三种形态,是对“数字按像素处理、字符串原样透传、缺省时回退默认值”这一规则的直接验证:

it('should support customizing the beam size', async () => {
  const { container, rerender } = render(
    <BorderBeam size={160}>
      <div>
        <span>content</span>
      </div>
    </BorderBeam>,
  );

  await waitFor(() => {
    // 数字 160 -> CSS 变量值为 '160px'
    expect(getBeamElement(container).style.getPropertyValue(varName('size'))).toBe('160px');
  });

  rerender(
    <BorderBeam size="12em">
      <div>
        <span>content</span>
      </div>
    </BorderBeam>,
  );

  // 字符串 '12em' 原样写入
  expect(getBeamElement(container).style.getPropertyValue(varName('size'))).toBe('12em');

  rerender(
    <BorderBeam>
      <div>
        <span>content</span>
      </div>
    </BorderBeam>,
  );

  // 不传 size 时不写变量,CSS 侧回退到默认 100px
  expect(getBeamElement(container).style.getPropertyValue(varName('size'))).toBe('');
});

结论与文档描述完全一致:

  • size={160} 最终成为 CSS 变量 160px(数字补 px);
  • size="12em" 原样写入,可用于相对字号的响应式场景;
  • 不传 size 时组件不写该变量,由 style/index.tsvarRef('size', '100px') 的默认值兜底为 100px

6. 实践建议小结

  • 基准值:不传 size,使用默认 100px,覆盖绝大多数卡片场景。
  • 密集小卡片:参考官方示例取 56 左右,避免流光段在小容器上比例失衡。
  • 大横幅 / 全宽面板:参考官方示例取 160 左右,让可见段与容器宽度相称。
  • 硬约束:保持 size < 2 × min(width, height)(短边为遮罩覆盖层,受 outset 影响),否则流光扫过对边时会上下(或左右)同时点亮。
  • 前提条件size 只对能成功“挂载”的容器生效——被包裹的 children 必须是原生 DOM 元素或正确透传 ref 的组件,且该节点通常需要提供 position: relative 定位上下文;否则流光层无法插入,size 自然无从谈起(详见 index.zh-CN.md FAQ「为什么 BorderBeam 没有效果?」)。

参考文件

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