wui
组件

闪光按钮 Shiny Button

带有连续平滑流动扫光效果的高亮展示型按钮,用于强化页面核心行动点(CTA)与转化入口。

第三方依赖 · motion

基础用法

最简单的闪光按钮形态。内置流动的斜向光斑扫描动画:

Loading…

安装与引入

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

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

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

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

export interface ShinyButtonProps extends HTMLMotionProps<"button"> {
  /** Seconds for one full light sweep across the button. @default 3 */
  speed?: number
  /** Pause between sweeps, in seconds. @default 1.2 */
  gap?: number
  // Narrows motion's children type (which allows MotionValue) back to ReactNode.
  children?: React.ReactNode
}

/**
 * A "fancy" template component: a filled button with a light sweep that
 * continuously glides across it. Effect components like this are opt-in and are
 * allowed to be more opinionated than the core UI — but they still gate motion
 * on `prefers-reduced-motion`. Copy it as a starting point for your own effects.
 */
function ShinyButton({
  className,
  speed = 3,
  gap = 1.2,
  children,
  ...props
}: ShinyButtonProps) {
  const reduceMotion = useReducedMotion()

  return (
    <motion.button
      data-slot="shiny-button"
      whileTap={reduceMotion ? undefined : { scale: 0.97 }}
      className={cn(
        "relative inline-flex h-10 shrink-0 items-center justify-center gap-2 overflow-hidden whitespace-nowrap rounded-lg border border-primary/20 bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
        className
      )}
      {...props}
    >
      <span className="relative z-10 inline-flex items-center gap-2">
        {children}
      </span>
      <motion.span
        aria-hidden
        data-slot="shiny-button-sheen"
        className="pointer-events-none absolute inset-y-0 left-0 w-1/2"
        style={{
          background:
            "linear-gradient(110deg, transparent 25%, var(--shine) 50%, transparent 75%)",
        }}
        initial={{ x: "-120%" }}
        animate={reduceMotion ? undefined : { x: "220%" }}
        transition={{
          duration: speed,
          ease: [0.45, 0, 0.25, 1],
          repeat: Infinity,
          repeatDelay: gap,
        }}
      />
    </motion.button>
  )
}

export { ShinyButton }

属性 Props

ShinyButton 接受以下配置属性,并继承 motion.button 的全部 HTML 属性与动画属性:

属性类型默认值说明
speednumber3光斑扫过整个按钮所需的周期时间(秒)。数值越小扫光越快。
gapnumber1.2每次扫光完成后的间隔静止时间(秒)。
childrenReact.ReactNode—按钮内部渲染的内容,通常包含文字或图标。
disabledbooleanfalse是否禁用按钮。禁用时将自动隐藏扫光图层并阻止交互。
classNamestring—应用于按钮容器的额外 CSS 类名,可用于自定义尺寸、圆角与背景色。

事件 Events

ShinyButton 继承原生按钮及 Motion 包装层的所有事件回调:

属性类型默认值说明
onClick(event: React.MouseEvent<HTMLButtonElement>) => void—用户点击或通过键盘激活按钮时触发。
onFocus(event: React.FocusEvent<HTMLButtonElement>) => void—按钮获得焦点时触发。
onBlur(event: React.FocusEvent<HTMLButtonElement>) => void—按钮失去焦点时触发。
onHoverStart(event: MouseEvent, info: EventInfo) => void—鼠标指针移入按钮区域时触发(Motion 回调)。
onHoverEnd(event: MouseEvent, info: EventInfo) => void—鼠标指针离开按钮区域时触发(Motion 回调)。

使用场景与设计规范

ShinyButton 属于展示型组件(Fancy 系列),具有极强的视觉吸引力。

  • 使用场景:
    • 核心转化 CTA:官网首页首屏的“免费试用”、“即刻体验”、“快速部署”等主行动点。
    • 付费与升级通道:价格方案对比卡片中的“升级专业版”或限时折扣引导。
    • 新功能与早鸟公告:引导用户体验实验性功能的醒目标签按钮。
  • 克制使用原则:
    • 同一视口中切忌大面积平铺多个闪光按钮,否则会分散用户注意力造成严重视觉疲劳。
    • 普通表单、密集表格操作列以及次要取消操作请使用基础的 Button。
  • 动效与色彩适配:
    • 光效层使用半透明渐变叠加在背景之上,在深色(Dark Mode)与浅色(Light Mode)模式下均能保持优雅的对比度。

场景示例

定价与方案升级对比

在定价卡片中将核心推荐方案的操作按钮升级为 ShinyButton,快速建立视觉层级差异:

Loading…

内测招募模块

与标题、说明和次要操作组合,构建克制但醒目的转化模块:

Loading…

尺寸与形状变体

通过 Tailwind 类名轻松调整尺寸与圆角(如胶囊型 Pill):

Loading…

无障碍与交互 Accessibility

  • 动效降级保障:组件内部使用 useReducedMotion 监听操作系统偏好。当用户开启了“减少动态效果”时,扫光动画自动关闭,按压缩放动效自动移除,组件退化为静态高对比度按钮。
  • 键盘焦点:支持标准的 Tab 聚焦以及 Enter / Space 激活,焦点轮廓清晰可见且不会被扫光层遮盖。
  • 非侵入式光斑:扫光层标记为 aria-hidden="true" 并配置 pointer-events-none,绝不干扰文字选区与屏幕阅读器朗读。