wui
组件

边框光束 Border Beam

沿元素边框循环流动的一段细光束,用于强调推荐项、处理中状态或需要吸引注意的输入区域。

第三方依赖 · motion

基础用法

把 BorderBeam 作为最后一个子元素放进带圆角的 relative 容器中,光束会贴合容器边框并继承其圆角。下例在点击发送后显示光束,表示内容正在生成:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/border-beam
安装基础依赖与 Motion 动效库
pnpm add motion clsx tailwind-merge
复制组件源码到 components/ui/border-beam.tsx
components/ui/border-beam.tsx
"use client"

import * as React from "react"
import { motion, useReducedMotion, type Transition } from "motion/react"

import { cn } from "@/lib/utils"

export interface BorderBeamProps
  extends Omit<React.ComponentProps<"div">, "children"> {
  /** Length of the beam along the border, in pixels. @default 80 */
  size?: number
  /** Seconds for one full lap. @default 6 */
  duration?: number
  /** Seconds to offset the start, useful to stagger several beams. @default 0 */
  delay?: number
  /** Thickness of the beam; match the border width of the parent. @default 1 */
  borderWidth?: number
  /** Beam colour. Any CSS colour or variable. @default "var(--primary)" */
  color?: string
  /** Travel counter-clockwise. @default false */
  reverse?: boolean
  /** Starting position along the border, from 0 to 100. @default 0 */
  initialOffset?: number
  /** Overrides the looping transition. */
  transition?: Transition
}

/**
 * A short light beam that travels along the border of its parent. Place it as
 * the last child of a `relative` element with a border radius; the beam sits
 * exactly on the parent's border and inherits its radius.
 */
function BorderBeam({
  size = 80,
  duration = 6,
  delay = 0,
  borderWidth = 1,
  color = "var(--primary)",
  reverse = false,
  initialOffset = 0,
  transition,
  className,
  style,
  ...props
}: BorderBeamProps) {
  const ref = React.useRef<HTMLDivElement>(null)
  const reduceMotion = useReducedMotion()
  const [radius, setRadius] = React.useState(0)

  React.useEffect(() => {
    const node = ref.current
    if (!node) return
    setRadius(Number.parseFloat(getComputedStyle(node).borderTopLeftRadius))
  }, [])

  if (reduceMotion) return null

  const start = `${initialOffset}%`
  const end = `${reverse ? initialOffset - 100 : initialOffset + 100}%`

  return (
    <div
      ref={ref}
      aria-hidden="true"
      data-slot="border-beam"
      className={cn("pointer-events-none absolute rounded-[inherit]", className)}
      style={{
        inset: -borderWidth,
        padding: borderWidth,
        maskImage: "linear-gradient(#000 0 0), linear-gradient(#000 0 0)",
        maskClip: "content-box, border-box",
        maskComposite: "exclude",
        ...style,
      }}
      {...props}
    >
      <motion.div
        data-slot="border-beam-light"
        className="absolute aspect-square"
        style={{
          width: size,
          offsetPath: `rect(0 auto auto 0 round ${radius}px)`,
          background: `linear-gradient(${reverse ? "to right" : "to left"}, ${color}, transparent)`,
        }}
        initial={{ offsetDistance: start }}
        animate={{ offsetDistance: [start, end] }}
        transition={{
          repeat: Infinity,
          ease: "linear",
          duration,
          delay: -delay,
          ...transition,
        }}
      />
    </div>
  )
}

export { BorderBeam }

属性 Props

BorderBeam 支持以下配置属性,并继承原生 <div> 的其余 HTML 属性:

属性类型默认值说明
sizenumber80光束沿边框方向的长度(像素)。
durationnumber6绕边框一圈的时长(秒)。
delaynumber0起始进度偏移(秒),多条光束错开时使用,例如 `duration / 2`。
borderWidthnumber1光束粗细,应与父元素的边框宽度一致。
colorstring"var(--primary)"光束颜色,支持任意 CSS 颜色或变量。光束尾部自动渐隐为透明。
reversebooleanfalse是否逆时针运动。
initialOffsetnumber0起始位置,取值 0–100,表示沿边框的百分比。
transitionTransition—覆盖默认的线性无限循环过渡。

使用场景与设计规范

  • 表达状态而非装饰:光束最适合表达“正在处理”或“推荐选择”。静态页面中同时出现的光束不应超过一处。
  • 保持克制:默认使用主题色且只有 1px 宽,尾部渐隐。不建议使用多色渐变或加粗光束,以免喧宾夺主。
  • 放在边框上:光束会覆盖在父元素的边框位置,因此父元素不能设置 overflow: hidden,否则光束会被裁剪。需要裁剪内容时,请在内部再包一层。

场景示例

推荐套餐

两条相同速度、错开半圈的光束(delay={duration / 2})让推荐套餐的边框更均匀地流动:

Loading…

无障碍与交互 Accessibility

  • 纯装饰元素:光束设置了 aria-hidden 与 pointer-events: none,不影响点击与读屏。其表达的状态(如“正在生成”)应同时通过文字呈现。
  • 减少动态效果:开启 prefers-reduced-motion 时不渲染光束,请确保推荐项仍有边框颜色、标签等静态区分。
  • 浏览器支持:光束路径基于 CSS offset-path: rect() 与 mask-composite,需要较新的 Chrome、Edge、Safari 与 Firefox。