Badge 徽章
用于显示状态、标签或数字的小型标识组件,支持轮廓、填充和促销三种样式变体,促销徽章还支持四种不同的促销类型和专属图标。
何时使用
- 需要突出显示产品折扣、优惠信息时
- 标识用户会员等级或特殊权益时
- 显示产品状态标签(新品、热门、限量等)
- 需要在内容旁边添加数量或计数时
- 标识特定类别或分类信息时
快速开始
安装
pnpm
pnpm add @anker-in/headless-ui基础用法
import { Badge } from '@anker-in/headless-ui'
export default function App() {
return <Badge>New</Badge>
}演示
基本用法
Badge 组件的基本用法示例
基础变体
促销徽章类型
尺寸对比
实际应用场景
AeroFit 2 Earbuds
$79.99
变体(Variants)
Badge 提供三种主要变体:
Outline - 轮廓样式
默认样式,使用品牌色轮廓,透明背景。
<Badge variant="outline">New</Badge>
<Badge variant="outline">20% OFF</Badge>
<Badge variant="outline">Limited</Badge>适用场景:
- 产品状态标签(新品、热门、限量等)
- 分类标签
- 一般信息标识
Fill - 填充样式
使用品牌色渐变背景,白色文字。
<Badge variant="fill">Sale</Badge>
<Badge variant="fill">15% OFF</Badge>
<Badge variant="fill">Bestseller</Badge>适用场景:
- 折扣优惠信息
- 重要促销标识
- 需要更强视觉冲击的场景
Promotional - 促销样式
特殊的促销徽章样式,左侧带有装饰图标,支持四种促销类型。
<Badge variant="promotional" promotionalType="plus-member">
Plus Member
</Badge>
<Badge variant="promotional" promotionalType="regular-member">
VIP Member
</Badge>
<Badge variant="promotional" promotionalType="regular-discount">
Limited Offer
</Badge>
<Badge variant="promotional" promotionalType="time-limited-discount">
Flash Sale
</Badge>适用场景:
- 会员等级标识
- 会员专享权益
- 特殊促销活动
- 限时优惠标识
促销类型(Promotional Types)
当使用 variant="promotional" 时,可以通过 promotionalType 指定不同的促销类型,每种类型都有独特的样式和图标:
Plus Member - Plus 会员
使用深色渐变背景(从金色到深色),配合金色渐变的专属图标。
<Badge variant="promotional" promotionalType="plus-member">
Plus Member
</Badge>样式特点:
- 背景:深色渐变 (
from-[#6E6754] via-[#4A453A] to-[#070708]) - 图标:金色渐变徽章图标
- 文字:白色
使用场景:
- Plus 会员标识
- VIP 高级会员
- 专属权益标识
- 联名款产品标签
Regular Member - 普通会员
使用品牌色填充背景,配合浅蓝色徽章图标。
<Badge variant="promotional" promotionalType="regular-member">
VIP Member
</Badge>样式特点:
- 背景:品牌色渐变
- 图标:浅蓝色徽章图标
- 文字:白色
使用场景:
- 普通会员标识
- 会员专享价格
- 会员专属内容
Regular Discount - 普通折扣
使用品牌色填充背景,配合折扣标识图标。
<Badge variant="promotional" promotionalType="regular-discount">
Premium
</Badge>样式特点:
- 背景:品牌色渐变
- 图标:折扣百分比图标
- 文字:白色
使用场景:
- 常规促销活动
- 折扣信息
- 特价标签
- 限量版标识
Time-Limited Discount - 限时折扣
使用品牌色填充背景,配合闪电图标。
<Badge variant="promotional" promotionalType="time-limited-discount">
Flash Sale
</Badge>样式特点:
- 背景:品牌色渐变
- 图标:闪电图标
- 文字:白色
使用场景:
- 限时闪购
- 秒杀活动
- 早鸟特惠
- 时间敏感的促销
向后兼容性:如果使用 variant="promotional" 但不指定 promotionalType,将默认使用 regular-discount 样式。
尺寸(Sizes)
Badge 提供两种尺寸,并支持响应式调整:
<Badge size="sm">Small Badge</Badge>
<Badge size="lg">Large Badge</Badge>尺寸规格
| 尺寸 | 基础高度 | 大屏幕高度 (≥1920px) | 文本大小 | 适用场景 |
|---|---|---|---|---|
sm | 24px | 24px | 14px | 紧凑布局、表格、列表 |
lg | 24px | 28px | 14px / 16px | 默认大小,产品卡片 |
响应式行为
- < 1920px:
lg尺寸高度为 24px,文字 14px - ≥ 1920px (lg-desktop):
lg尺寸高度增加到 28px,文字 16px
促销徽章 (variant="promotional") 的内容区域会向右偏移,为左侧的装饰图标留出空间:
sm尺寸:左侧内边距 24pxlg尺寸:左侧内边距 24px (基础) / 28px (lg-desktop)
Props API
Badge Props
| 属性 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| variant | 'outline' | 'fill' | 'promotional' | 'outline' | ❌ | 徽章变体样式 |
| size | 'sm' | 'lg' | 'lg' | ❌ | 徽章尺寸 |
| promotionalType | 'plus-member' | 'regular-member' | 'regular-discount' | 'time-limited-discount' | - | ❌ | 促销类型(仅在 variant="promotional" 时生效) |
| children | ReactNode | - | ✅ | 徽章内容 |
| className | string | - | ❌ | 自定义 CSS 类名 |
继承的 Props
Badge 组件继承所有标准 HTML <div> 元素的属性。
类型定义
import { VariantProps } from 'class-variance-authority'
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {
/**
* 促销类型,仅在 variant="promotional" 时生效
* - plus-member: Plus 会员(深色渐变)
* - regular-member: 普通会员(品牌色)
* - regular-discount: 普通折扣(品牌色)
* - time-limited-discount: 限时折扣(品牌色)
*/
promotionalType?:
| 'plus-member'
| 'regular-member'
| 'regular-discount'
| 'time-limited-discount'
}
function Badge({
className,
size,
variant = 'outline',
promotionalType,
...props
}: BadgeProps): JSX.Element
export default Badge使用示例
产品卡片中的应用
import { Badge } from '@anker-in/headless-ui'
export default function ProductCard() {
return (
<div className="rounded-lg border p-4">
{/* 多个徽章组合 */}
<div className="mb-3 flex gap-2">
<Badge size="sm" variant="fill">
20% OFF
</Badge>
<Badge size="sm" variant="outline">
New
</Badge>
</div>
<h3 className="mb-2 font-medium">AeroFit 2 Earbuds</h3>
<p className="mb-3 text-sm text-gray-600">
无线降噪耳机,续航长达 8 小时
</p>
{/* 促销徽章 */}
<div className="mb-3">
<Badge variant="promotional" promotionalType="plus-member">
Plus Member Price
</Badge>
</div>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold">$79.99</span>
<span className="text-gray-400 line-through">$99.99</span>
</div>
</div>
)
}会员权益标识
import { Badge } from '@anker-in/headless-ui'
export default function MembershipBadges() {
return (
<div className="space-y-4">
{/* Plus 会员 */}
<div className="flex items-center gap-3">
<Badge variant="promotional" promotionalType="plus-member">
Plus Member
</Badge>
<span className="text-sm text-gray-600">
享受专属折扣和提前购买权
</span>
</div>
{/* 普通会员 */}
<div className="flex items-center gap-3">
<Badge variant="promotional" promotionalType="regular-member">
VIP Member
</Badge>
<span className="text-sm text-gray-600">会员专享价格</span>
</div>
</div>
)
}促销活动标签
import { Badge } from '@anker-in/headless-ui'
export default function PromotionalLabels() {
return (
<div className="space-y-3">
{/* 限时闪购 */}
<div className="flex items-center justify-between rounded-lg bg-gray-50 p-4">
<span className="font-medium">MacBook Pro 限时优惠</span>
<Badge variant="promotional" promotionalType="time-limited-discount">
闪购中
</Badge>
</div>
{/* 会员专享 */}
<div className="flex items-center justify-between rounded-lg bg-gray-50 p-4">
<span className="font-medium">Anker 充电宝</span>
<Badge variant="promotional" promotionalType="regular-member">
会员专享
</Badge>
</div>
{/* 特价促销 */}
<div className="flex items-center justify-between rounded-lg bg-gray-50 p-4">
<span className="font-medium">无线充电器套装</span>
<Badge variant="promotional" promotionalType="regular-discount">
限时优惠
</Badge>
</div>
</div>
)
}状态标签
import { Badge } from '@anker-in/headless-ui'
export default function StatusBadges() {
return (
<div className="space-y-3">
{/* 产品列表 */}
<div className="flex items-center justify-between">
<span>PowerCore 20K 移动电源</span>
<div className="flex gap-2">
<Badge variant="outline" size="sm">
Hot
</Badge>
<Badge variant="fill" size="sm">
Bestseller
</Badge>
</div>
</div>
<div className="flex items-center justify-between">
<span>SoundCore Liberty 4 耳机</span>
<div className="flex gap-2">
<Badge variant="outline" size="sm">
New
</Badge>
<Badge variant="fill" size="sm">
25% OFF
</Badge>
</div>
</div>
</div>
)
}通知计数
import { Badge } from '@anker-in/headless-ui'
import { Bell, ShoppingCart } from 'lucide-react'
export default function NotificationBadges() {
return (
<div className="flex gap-6">
{/* 通知图标 */}
<div className="relative">
<Bell className="h-6 w-6" />
<Badge
size="sm"
variant="fill"
className="absolute -right-2 -top-2 h-5 min-w-[20px] rounded-full px-1 text-xs"
>
5
</Badge>
</div>
{/* 购物车图标 */}
<div className="relative">
<ShoppingCart className="h-6 w-6" />
<Badge
size="sm"
variant="fill"
className="absolute -right-2 -top-2 h-5 min-w-[20px] rounded-full px-1 text-xs"
>
3
</Badge>
</div>
</div>
)
}动态状态
import { useState } from 'react'
import { Badge } from '@anker-in/headless-ui'
type UserTier = 'free' | 'vip' | 'plus'
export default function DynamicBadge() {
const [tier, setTier] = useState<UserTier>('free')
const renderBadge = () => {
if (tier === 'plus') {
return (
<Badge variant="promotional" promotionalType="plus-member">
Plus Member
</Badge>
)
}
if (tier === 'vip') {
return (
<Badge variant="promotional" promotionalType="regular-member">
VIP Member
</Badge>
)
}
return <Badge variant="outline">Free User</Badge>
}
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<span className="font-medium">当前会员等级:</span>
{renderBadge()}
</div>
<div className="flex gap-2">
<button
onClick={() => setTier('free')}
className="rounded bg-gray-200 px-3 py-1 text-sm"
>
免费用户
</button>
<button
onClick={() => setTier('vip')}
className="rounded bg-gray-200 px-3 py-1 text-sm"
>
VIP 会员
</button>
<button
onClick={() => setTier('plus')}
className="rounded bg-gray-200 px-3 py-1 text-sm"
>
Plus 会员
</button>
</div>
</div>
)
}列表筛选标签
import { useState } from 'react'
import { Badge } from '@anker-in/headless-ui'
import { X } from 'lucide-react'
export default function FilterBadges() {
const [selectedFilters, setSelectedFilters] = useState<string[]>([
'Wireless',
'Under $100',
])
const removeFilter = (filter: string) => {
setSelectedFilters((prev) => prev.filter((f) => f !== filter))
}
return (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700">已选筛选条件:</h4>
<div className="flex flex-wrap gap-2">
{selectedFilters.map((filter) => (
<Badge
key={filter}
variant="outline"
className="group relative cursor-pointer pr-7 hover:border-red-500"
onClick={() => removeFilter(filter)}
>
{filter}
<X className="absolute right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 text-gray-400 group-hover:text-red-500" />
</Badge>
))}
</div>
</div>
)
}可访问性
语义化
Badge 使用 <div> 元素实现,建议根据使用场景添加适当的 ARIA 属性。
// 用于装饰性标签
<Badge aria-hidden="true">New</Badge>
// 用于传达重要信息
<Badge role="status" aria-label="有 5 条新通知">
5
</Badge>
// 用于交互式标签(可关闭)
<Badge
role="button"
tabIndex={0}
aria-label="移除筛选条件: Wireless"
onClick={removeFilter}
>
Wireless <X />
</Badge>颜色对比度
Badge 组件的所有变体都经过设计以确保符合 WCAG AA 标准:
- Outline 变体:品牌色轮廓与背景的对比度至少 3:1
- Fill 变体:白色文字与渐变背景的对比度至少 4.5:1
- Promotional 变体:白色文字与背景的对比度至少 4.5:1
最佳实践
提供清晰的文本内容
确保徽章的文本内容简洁明了,能够传达明确的信息。
// ✅ 推荐:清晰的文本
<Badge>20% OFF</Badge>
<Badge>New Arrival</Badge>
// ❌ 避免:模糊的文本
<Badge>!</Badge>
<Badge>*</Badge>为数字徽章添加 ARIA 标签
当徽章仅显示数字时,添加 aria-label 说明数字的含义。
<Badge role="status" aria-label="5 条未读消息">
5
</Badge>避免过度使用
不要在单个界面上使用过多的徽章,以免分散用户注意力。
// ✅ 推荐:适度使用
<div>
<Badge>New</Badge>
<Badge>20% OFF</Badge>
</div>
// ❌ 避免:过度使用
<div>
<Badge>New</Badge>
<Badge>Hot</Badge>
<Badge>Sale</Badge>
<Badge>Limited</Badge>
<Badge>Trending</Badge>
<Badge>Popular</Badge>
</div>确保交互式徽章可键盘访问
如果徽章可点击或交互,确保支持键盘操作。
<Badge
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Remove
</Badge>最佳实践
✅ 推荐做法
- 使用语义化的文本:徽章内容应该简洁明了,传达清晰的信息
- 选择合适的变体:根据重要性和使用场景选择 outline、fill 或 promotional
- 保持一致性:在整个应用中使用一致的徽章样式和颜色
- 适度使用促销徽章:促销徽章视觉冲击力强,应谨慎使用避免过度
- 组合使用:在产品卡片中可以组合使用多个徽章展示不同信息
// ✅ 正确示例:清晰的信息层级
<div className="flex gap-2">
<Badge variant="fill">30% OFF</Badge>
<Badge variant="outline">Limited Time</Badge>
<Badge variant="outline">Hot</Badge>
</div>
// ✅ 正确示例:会员等级标识
<Badge variant="promotional" promotionalType="plus-member">
Plus Member Exclusive
</Badge>❌ 避免做法
- 不要使用空徽章:徽章内容应该有实际意义,避免仅用于装饰
- 不要堆叠过多徽章:单个元素上徽章数量不应超过 3 个
- 不要使用模糊的文本:避免使用单个字符或不明确的缩写
- 不要混淆促销类型:每种 promotionalType 都有特定用途,不应混用
- 不要忽略响应式:注意不同屏幕尺寸下徽章的显示效果
// ❌ 错误示例:过多徽章
<div className="flex gap-2">
<Badge>New</Badge>
<Badge>Hot</Badge>
<Badge>Sale</Badge>
<Badge>Limited</Badge>
<Badge>Trending</Badge> {/* 太多了! */}
</div>
// ❌ 错误示例:模糊的文本
<Badge>!</Badge> {/* 什么意思? */}
<Badge>*</Badge> {/* 不明确 */}
// ✅ 正确示例:适度使用
<div className="flex gap-2">
<Badge variant="fill">25% OFF</Badge>
<Badge variant="outline">New</Badge>
</div>常见问题
如何自定义徽章的圆角?
通过 className 覆盖默认的 rounded-[16px] 样式:
<Badge className="rounded-full">Fully Rounded</Badge>
<Badge className="rounded-md">Medium Rounded</Badge>
<Badge className="rounded-none">No Rounded</Badge>促销徽章的图标可以自定义吗?
目前促销徽章的图标是固定的,每种 promotionalType 对应一个特定图标。如果需要完全自定义图标,建议使用 variant="outline" 或 variant="fill" 并手动添加自定义图标:
import { Badge } from '@anker-in/headless-ui'
import { Star } from 'lucide-react'
<Badge variant="fill" className="flex items-center gap-1">
<Star className="h-3 w-3" />
<span>Custom Icon</span>
</Badge>如何实现可关闭的徽章?
添加点击事件和关闭图标:
import { useState } from 'react'
import { Badge } from '@anker-in/headless-ui'
import { X } from 'lucide-react'
export default function ClosableBadge() {
const [visible, setVisible] = useState(true)
if (!visible) return null
return (
<Badge
variant="outline"
className="group relative cursor-pointer pr-7"
onClick={() => setVisible(false)}
>
Tag Name
<X className="absolute right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 text-gray-400 group-hover:text-red-500" />
</Badge>
)
}徽章内容过长时如何处理?
使用 max-w-* 和 truncate 类来限制宽度和截断文本:
<Badge className="max-w-[120px] truncate" title="Very Long Badge Text That Exceeds Width">
Very Long Badge Text That Exceeds Width
</Badge>Plus 会员徽章为什么使用深色渐变?
Plus 会员是最高等级的会员类型,使用深色渐变(金色到深色)和金色图标可以体现其尊贵感和独特性,与其他促销类型形成明显的视觉区分。这种设计在电商和会员系统中是常见的最佳实践。
如何在移动端优化徽章显示?
Badge 组件默认支持响应式,但在移动端可能需要调整尺寸:
// 移动端使用 sm 尺寸,桌面端使用 lg 尺寸
<Badge size="sm" className="tablet:size-lg">
Responsive Badge
</Badge>
// 或者通过 className 自定义移动端样式
<Badge className="text-xs tablet:text-sm">
Mobile Optimized
</Badge>促销徽章可以不显示图标吗?
促销徽章 (variant="promotional") 的图标是自动添加的,无法通过 props 关闭。如果不需要图标,建议使用 variant="fill" 替代:
// 如果不需要图标,使用 fill 变体
<Badge variant="fill">No Icon Badge</Badge>
// 而不是 promotional 变体
<Badge variant="promotional" promotionalType="regular-discount">
With Icon {/* 会自动显示图标 */}
</Badge>相关资源
更新日志
v2.0.0 (2025-01-17)
- 🎉 初始版本发布
- ✨ 支持三种变体(outline, fill, promotional)
- ✨ 支持两种尺寸(sm, lg)并支持响应式
- ✨ 促销徽章支持四种促销类型和专属图标
- 🎨 Plus 会员使用深色渐变独特设计
- ♿ 完整的可访问性支持
- 📱 响应式设计,移动端友好
- 📝 完善的 TypeScript 类型定义
本文档有帮助吗?
在 GitHub 上反馈