Button 按钮
可点击的交互式按钮组件,支持多种样式、大小、状态和加载效果。按钮是用户界面中最常用的交互元素之一。
何时使用
- 需要触发一个业务逻辑操作时(如提交表单、删除数据、打开对话框)
- 需要引导用户完成主要操作时(如”立即购买”、“注册账号”)
- 需要在页面之间导航时(使用
as="a"属性) - 需要展示加载状态以提供视觉反馈时
快速开始
安装
pnpm
pnpm add @anker-in/headless-ui基础用法
import { Button } from '@anker-in/headless-ui'
export default function App() {
return <Button>立即购买</Button>
}演示
变体(Variants)
Button 组件提供 4 种视觉变体,适用于不同的视觉层级和使用场景。
Primary - 主要按钮
用于页面中最重要的操作,通常一个页面只有一个主要按钮。
<Button variant="primary">主要操作</Button>Secondary - 次要按钮
用于次要操作或与主要按钮配合使用。
<Button variant="secondary">次要操作</Button>Link - 链接按钮
用于不太重要的操作或文本内的链接样式按钮。自动带有右侧箭头图标。
<Button variant="link">了解更多</Button>Ghost - 幽灵按钮 ⚠️
Ghost 变体即将废弃,请使用 secondary 变体代替。
<Button variant="ghost">幽灵按钮</Button>尺寸(Sizes)
Button 组件提供多种尺寸选项以适应不同的 UI 密度和设备。
<Button size="sm">小尺寸</Button>
<Button size="base">基础尺寸</Button>
<Button size="lg">大尺寸(默认)</Button>
<Button size="icon">图标按钮</Button>响应式尺寸: lg 尺寸在超大屏幕(≥1920px)上会自动增大,以提供更好的视觉体验。
状态(States)
加载状态
显示加载指示器,同时禁用按钮交互。按钮会保持原始宽度以避免布局跳动。
<Button loading>加载中...</Button>自定义 Spinner
<Button
loading
spinner={
<svg className="animate-spin" width="16" height="16">
{/* 自定义 SVG */}
</svg>
}
>
处理中
</Button>禁用状态
禁用按钮交互,显示禁用样式。
<Button disabled>禁用按钮</Button>Hover 动画效果
支持滑动动画效果(仅 primary 和 secondary 变体)。
<Button hoverEffect="slide">Hover 滑动效果</Button>Props API
Button Props
| 属性 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| variant | 'primary' | 'secondary' | 'link' | 'ghost' | 'primary' | ❌ | 按钮变体 |
| size | 'sm' | 'base' | 'lg' | 'icon' | 'lg' | ❌ | 按钮尺寸 |
| hoverEffect | 'none' | 'slide' | 'none' | ❌ | Hover 动画效果(仅 primary/secondary) |
| loading | boolean | false | ❌ | 是否显示加载状态 |
| disabled | boolean | false | ❌ | 是否禁用按钮 |
| as | 'button' | 'a' | 'button' | ❌ | 渲染为按钮或链接 |
| href | string | - | ❌ | 链接地址(当 as="a" 时使用) |
| spinner | ReactNode | - | ❌ | 自定义加载图标 |
| iconClassName | string | - | ❌ | 图标的自定义类名 |
| children | ReactNode | - | ✅ | 按钮内容 |
| className | string | - | ❌ | 自定义 CSS 类名 |
| onClick | (e: MouseEvent) => void | - | ❌ | 点击事件回调 |
继承的 Props
Button 组件继承所有原生 <button> 元素的属性(如 type, name, value 等)。
类型定义
import { type VariantProps } from 'class-variance-authority'
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/** 设置按钮载入状态 */
loading?: boolean
/** 设置按钮失效状态 */
disabled?: boolean
/** 定义渲染的元素类型 */
as?: 'button' | 'a'
/** 链接地址(当 as='a' 时) */
href?: string
/** 图标类名 */
iconClassName?: string
/** 自定义 loading 图标 */
spinner?: React.ReactNode
/** Hover 动画效果 */
hoverEffect?: 'none' | 'slide'
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, size, loading, disabled, children, ...props }, ref) => {
// 组件实现
}
)
Button.displayName = 'Button'
export default Button使用示例
基础示例
最简单的按钮用法。
import { Button } from '@anker-in/headless-ui'
export default function Example1() {
return (
<Button onClick={() => alert('clicked!')}>
立即购买
</Button>
)
}带图标的按钮
在按钮中添加图标以增强视觉表达。
import { Button } from '@anker-in/headless-ui'
import { ShoppingCartIcon } from '@/icons'
export default function Example2() {
return (
<Button>
<ShoppingCartIcon className="mr-2" />
加入购物车
</Button>
)
}Link 变体自动图标: variant="link" 会自动在右侧添加箭头图标,无需手动添加。
作为链接使用
使用 as="a" 属性将按钮渲染为链接,适用于页面导航。
import { Button } from '@anker-in/headless-ui'
export default function Example3() {
return (
<Button as="a" href="/products" variant="link">
浏览全部产品
</Button>
)
}异步操作与加载状态
展示异步操作的加载状态。
import { useState } from 'react'
import { Button } from '@anker-in/headless-ui'
export default function Example4() {
const [loading, setLoading] = useState(false)
const handleSubmit = async () => {
setLoading(true)
try {
await fetch('/api/submit', { method: 'POST' })
alert('提交成功!')
} catch (error) {
alert('提交失败')
} finally {
setLoading(false)
}
}
return (
<Button loading={loading} onClick={handleSubmit}>
提交表单
</Button>
)
}表单中使用
在表单中使用不同类型的按钮。
import { Button } from '@anker-in/headless-ui'
export default function Example5() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// 处理表单提交
}
return (
<form onSubmit={handleSubmit}>
<input type="email" required />
<div className="mt-4 flex gap-3">
<Button type="submit" variant="primary">
提交
</Button>
<Button type="button" variant="secondary" onClick={() => console.log('取消')}>
取消
</Button>
<Button type="reset" variant="link">
重置
</Button>
</div>
</form>
)
}按钮组合
多个按钮组合使用时的视觉层级处理。
import { Button } from '@anker-in/headless-ui'
export default function Example6() {
return (
<div className="flex items-center gap-4">
{/* 主要操作 */}
<Button variant="primary" size="lg">
立即购买
</Button>
{/* 次要操作 */}
<Button variant="secondary" size="lg">
加入购物车
</Button>
{/* 辅助操作 */}
<Button variant="link" size="lg">
了解更多
</Button>
</div>
)
}可访问性
键盘支持
| 按键 | 说明 |
|---|---|
Enter | 激活按钮 |
Space | 激活按钮 |
Tab | 移动焦点到下一个可聚焦元素 |
Shift + Tab | 移动焦点到上一个可聚焦元素 |
ARIA 属性
Button 组件自动处理以下可访问性属性:
- 使用
aria-hidden隐藏视觉占位内容(加载状态时) - 使用
VisuallyHidden确保加载状态下屏幕阅读器仍能读取按钮文本 - 自动设置
disabled属性(禁用和加载状态)
推荐的 ARIA 用法
{/* 图标按钮必须提供 aria-label */}
<Button size="icon" aria-label="搜索">
<SearchIcon />
</Button>
{/* 动态内容需要 aria-live */}
<Button aria-live="polite" loading={loading}>
{loading ? '处理中...' : '提交'}
</Button>最佳实践
确保足够的点击区域
按钮的最小点击区域应为 44x44px(移动端)以符合 WCAG 2.1 标准。组件默认已满足此要求。
提供清晰的按钮文本
使用描述性的按钮文本,避免使用模糊的词汇如”点击这里”、“提交”。
{/* ❌ 不推荐 */}
<Button>提交</Button>
{/* ✅ 推荐 */}
<Button>提交订单</Button>使用视觉焦点指示器
组件已内置焦点环(focus-visible:ring-2),确保键盘用户能清楚看到当前焦点。
合理使用加载状态
在异步操作时始终显示加载状态,避免用户重复点击。
<Button
loading={isSubmitting}
disabled={isSubmitting}
onClick={handleSubmit}
>
{isSubmitting ? '提交中...' : '提交订单'}
</Button>测试屏幕阅读器
使用 VoiceOver(macOS)、NVDA(Windows)或 JAWS 测试按钮的可访问性。
避免仅用颜色区分状态
除了颜色变化,还应使用文本、图标等方式传达按钮状态。
最佳实践
✅ 推荐做法
- 一个页面一个主要按钮: 避免使用多个
primary按钮造成视觉混乱
<div>
<Button variant="primary">立即购买</Button>
<Button variant="secondary">加入购物车</Button>
</div>- 异步操作显示加载状态: 提供清晰的视觉反馈
<Button loading={loading} disabled={loading}>
{loading ? '处理中...' : '提交订单'}
</Button>- 使用语义化的按钮类型: 在表单中使用正确的
type属性
<Button type="submit">提交</Button>
<Button type="button" onClick={handleCancel}>取消</Button>
<Button type="reset">重置</Button>- 导航使用链接样式: 页面跳转使用
as="a"或variant="link"
<Button as="a" href="/products" variant="link">
查看全部产品
</Button>❌ 避免做法
- 不要在加载时允许交互: 必须同时设置
loading和disabled
{/* ❌ 错误:未禁用按钮 */}
<Button loading={loading} onClick={handleClick}>
提交
</Button>
{/* ✅ 正确:同时禁用交互 */}
<Button loading={loading} disabled={loading} onClick={handleClick}>
提交
</Button>- 不要省略图标按钮的标签: 图标按钮必须有
aria-label
{/* ❌ 错误 */}
<Button size="icon">
<SearchIcon />
</Button>
{/* ✅ 正确 */}
<Button size="icon" aria-label="搜索">
<SearchIcon />
</Button>- 不要使用模糊的按钮文本: 文本应明确说明操作结果
{/* ❌ 错误:文本模糊 */}
<Button>点击</Button>
<Button>确定</Button>
{/* ✅ 正确:文本明确 */}
<Button>删除账户</Button>
<Button>保存更改</Button>常见问题
如何自定义按钮的宽度?
使用 className 添加宽度样式:
<Button className="w-full">全宽按钮</Button>
<Button className="min-w-[200px]">最小宽度 200px</Button>如何在按钮中使用自定义图标?
直接将图标作为 children 传入:
<Button>
<CustomIcon className="mr-2" />
按钮文本
</Button>对于 link 变体,组件会自动在右侧添加箭头图标。如需自定义该图标的样式,使用 iconClassName 属性:
<Button variant="link" iconClassName="text-red-500">
自定义箭头颜色
</Button>Loading 状态时如何保持按钮宽度?
组件已自动处理,加载时会保持原始内容的宽度(通过 visibility: hidden 保留占位)。
如何实现按钮的防抖?
推荐在业务层实现防抖逻辑:
import { useMemo } from 'react'
import { Button } from '@anker-in/headless-ui'
import { debounce } from 'lodash'
export default function DebouncedButton() {
const handleClick = useMemo(
() => debounce(() => {
console.log('Debounced click')
}, 500),
[]
)
return <Button onClick={handleClick}>点击我</Button>
}按钮在 Safari 中样式异常?
Button 组件已针对 Safari 进行优化。如遇到样式问题,请检查:
- 是否使用了自定义
className覆盖了默认样式 - 父容器是否设置了影响布局的样式(如
display: grid) - 是否使用了浏览器默认样式重置(推荐使用 Tailwind 的 Preflight)
如何与 React Router 或 Next.js Link 配合使用?
方式 1: 使用 as="a" 属性
<Button as="a" href="/products">
查看产品
</Button>方式 2: 包裹 Link 组件 (推荐用于 Next.js)
import Link from 'next/link'
import { Button } from '@anker-in/headless-ui'
<Link href="/products" passHref legacyBehavior>
<Button as="a">查看产品</Button>
</Link>方式 3: 使用 className 包裹
import Link from 'next/link'
<Link href="/products">
<span className="inline-block">
<Button>查看产品</Button>
</span>
</Link>相关资源
- Storybook 文档 - 查看所有变体和交互示例
- GitHub 源代码 - 查看完整实现代码
- 无障碍性指南 (WCAG) - 了解更多无障碍标准
更新日志
v2.0.0 (2025-01-17)
- 🎉 完善组件文档,添加详细的使用指南和最佳实践
- ✨ 新增
hoverEffect属性支持滑动动画 - ✨ 新增
as属性替代已废弃的asChild - 🔧 改进无障碍性支持(加载状态下的屏幕阅读器体验)
- ⚠️ 破坏性变更: 废弃
ghost变体,请使用secondary代替 - ⚠️ 破坏性变更: 废弃
asChild属性,请使用as代替
本文档有帮助吗?
在 GitHub 上反馈