wui
组件

渐变环绕发光 Glow

让多色彩虹渐变或单色呼吸光效沿内容边缘持续环绕流动,形成柔和漫射光晕,用于打造高视觉吸引力的重点卡片与推荐位。

第三方依赖 · motion

基础用法

最简单的发光包装用法。组件提供多色流光(Rainbow)与单色呼吸(Solid)两种光效变体:

Loading…

安装与引入

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

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

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

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

export interface GlowProps extends Omit<HTMLMotionProps<"div">, "children"> {
  /** Content placed above the glow. */
  children: React.ReactNode
  /** Visual treatment for the illuminated edge. @default "rainbow" */
  variant?: "rainbow" | "solid"
  /** CSS color used by the solid variant. @default "var(--chart-1)" */
  color?: string
  /** Colors distributed around the rainbow edge. Defaults to the theme chart palette. */
  colors?: string[]
  /** Distance the light spreads beyond the edge, in pixels. @default 18 */
  spread?: number
  /** Width of the illuminated edge, in pixels. @default 1 */
  borderWidth?: number
  /** Peak opacity of the glow. @default 0.7 */
  glowOpacity?: number
  /** Gently pulse the solid variant. @default true */
  pulse?: boolean
  /** Seconds for one animation cycle. @default 4 */
  duration?: number
}

/** Adds an illuminated border and soft light around all four sides of content. */
function Glow({
  children,
  variant = "rainbow",
  color = "var(--chart-1)",
  colors = [
    "var(--chart-1)",
    "var(--chart-5)",
    "var(--chart-2)",
    "var(--chart-3)",
    "var(--chart-1)",
  ],
  spread = 18,
  borderWidth = 1,
  glowOpacity = 0.7,
  pulse = true,
  duration = 4,
  className,
  ...props
}: GlowProps) {
  const reduceMotion = useReducedMotion()
  const shouldPulse = pulse && !reduceMotion
  const lightSpread = Math.max(0, spread)
  const edgeWidth = Math.max(0, borderWidth)
  const gradient = (angle: number) =>
    `conic-gradient(from ${angle}deg, ${colors.join(", ")})`
  const mask =
    "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)"
  const ringStyle: React.CSSProperties = {
    inset: -edgeWidth,
    padding: edgeWidth,
    maskImage: mask,
    maskComposite: "exclude",
    WebkitMaskImage: mask,
    WebkitMaskComposite: "xor",
  }

  const rainbowTransition = reduceMotion
    ? { duration: 0 }
    : { duration, ease: "linear" as const, repeat: Infinity }

  return (
    <motion.div
      data-slot="glow"
      className={cn("relative isolate rounded-lg", className)}
      {...props}
    >
      {variant === "rainbow" ? (
        <>
          <motion.div
            aria-hidden="true"
            data-slot="glow-light"
            className="pointer-events-none absolute rounded-[inherit]"
            style={{
              ...ringStyle,
              backgroundImage: gradient(0),
              filter: `blur(${lightSpread * 0.45}px)`,
            }}
            initial={false}
            animate={
              reduceMotion
                ? { opacity: glowOpacity }
                : {
                    backgroundImage: [gradient(0), gradient(360)],
                    opacity: glowOpacity,
                  }
            }
            transition={rainbowTransition}
          />
          <motion.div
            aria-hidden="true"
            data-slot="glow-edge"
            className="pointer-events-none absolute rounded-[inherit]"
            style={{ ...ringStyle, backgroundImage: gradient(0) }}
            initial={false}
            animate={
              reduceMotion
                ? { opacity: 1 }
                : {
                    backgroundImage: [gradient(0), gradient(360)],
                    opacity: 1,
                  }
            }
            transition={rainbowTransition}
          />
        </>
      ) : (
        <motion.div
          aria-hidden="true"
          data-slot="glow-light"
          className="pointer-events-none absolute rounded-[inherit]"
          style={{
            inset: -edgeWidth,
            border: `${edgeWidth}px solid ${color}`,
            boxShadow: `0 0 ${lightSpread * 0.45}px ${color}, 0 0 ${lightSpread}px ${color}`,
          }}
          initial={false}
          animate={
            shouldPulse
              ? {
                  opacity: [
                    glowOpacity * 0.55,
                    glowOpacity,
                    glowOpacity * 0.55,
                  ],
                }
              : { opacity: glowOpacity }
          }
          transition={
            shouldPulse
              ? { duration, ease: "easeInOut", repeat: Infinity }
              : { duration: 0 }
          }
        />
      )}
      <div data-slot="glow-content" className="relative z-10 rounded-[inherit]">
        {children}
      </div>
    </motion.div>
  )
}

export { Glow }

属性 Props

Glow 支持以下流光与发光配置,并继承 motion.div 的全部 HTML 属性:

属性类型默认值说明
childrenReact.ReactNode—发光边框包裹的内容元素(卡片、徽标、横幅等)。
variant"rainbow" | "solid""rainbow"发光视觉变体:rainbow(多色环绕流光)或 solid(单色脉冲呼吸光)。
colorstring"var(--chart-1)"在 solid 变体下生效的边框与发光主色(支持 OKLCH / RGB / HEX / CSS 变量)。
colorsstring[]["var(--chart-1)", "var(--chart-5)", "var(--chart-2)", "var(--chart-3)", "var(--chart-1)"]在 rainbow 变体下沿圆周渐变分布的色彩序列(conic-gradient)。
spreadnumber18光晕向外扩散的模糊半径(像素 px)。数值越大漫射光越柔和广阔。
borderWidthnumber1发光细边框的物理粗细(像素 px)。
glowOpacitynumber0.7发光漫射层的最大峰值不透明度(0~1)。
pulsebooleantrue是否在 solid 变体下开启明暗呼吸动效。
durationnumber4完成一次完整旋转或呼吸循环所需的周期时间(秒)。
classNamestring—应用于外层发光容器的额外 CSS 类名(需指定圆角如 rounded-xl 以便光晕贴合)。

事件 Events

Glow 作为视觉展示容器透传标准的 React / Motion 容器事件:

属性类型默认值说明
onHoverStart(event: MouseEvent, info: EventInfo) => void—指针进入发光容器时触发。
onHoverEnd(event: MouseEvent, info: EventInfo) => void—指针离开发光容器时触发。
onClick(event: React.MouseEvent<HTMLDivElement>) => void—点击发光卡片容器时触发。

使用场景与设计规范

Glow 属于高吸引力的视觉强调组件(Fancy 系列)。

  • 适用场景:
    • 核心推荐方案(Pro / Featured Plan):定价页中将“最推荐”套餐包裹 Glow,迅速建立视觉第一焦点。
    • 旗舰功能或 AI 核心卡片:突出具有代表性的技术亮点。
    • 顶部醒目状态胶囊 / 公告条(Pills):新版本发布通知、系统运行健康状态等。
  • 色彩与层级搭配:
    • Glow 内部使用智能 CSS 遮罩(Mask exclude),光晕与边框精确贴合在内容最外缘,内部内容保持纯净背景,不会发生色彩溢出污染文字。
    • 务必确保传递给 Glow 的 className 圆角与内部卡片保持一致(例如两者均为 rounded-2xl),以获得最平滑的边缘贴合。
  • 克制设计原则:
    • 同一屏内通常只推荐配置 1~2 个发光焦点,避免光晕泛滥导致用户无法聚焦核心内容。

场景示例

旗舰推荐定价卡片

在价格对比表中为核心方案包裹流光边框,强化转化意图:

Loading…

状态指示与公告胶囊(Pills)

在小型胶囊标签或系统状态徽标上使用单色呼吸光或彩虹流光:

Loading…

无障碍与交互 Accessibility

  • 自动动效降级:当操作系统开启了“减少动态效果(prefers-reduced-motion)”时,彩虹旋转与呼吸脉冲动画自动停止,保持静态柔和光晕,杜绝视觉干扰。
  • 非侵入式光效:发光层标注为 aria-hidden="true" 并配置 pointer-events-none,不遮挡卡片内部的文本选区与按钮点击。
  • 可读性保障:发光位于底层外周,内容区域通过实体背景色隔绝,文字始终保持高对比度与清晰可读。