Skip to Content
@anker-in/headless-ui 2.0 is released 🎉

Badge 徽章

用于显示状态、标签或数字的小型标识组件,支持轮廓、填充和促销三种样式变体,促销徽章还支持四种不同的促销类型和专属图标。

何时使用

  • 需要突出显示产品折扣、优惠信息时
  • 标识用户会员等级或特殊权益时
  • 显示产品状态标签(新品、热门、限量等)
  • 需要在内容旁边添加数量或计数时
  • 标识特定类别或分类信息时

快速开始

安装

pnpm add @anker-in/headless-ui

基础用法

import { Badge } from '@anker-in/headless-ui' export default function App() { return <Badge>New</Badge> }

演示

Badge 组件演示

徽章组件的各种变体、尺寸和促销类型展示

基本用法

Badge 组件的基本用法示例

100% (1920px)

基础变体

New
Sale
Plus Member

促销徽章类型

Plus专享
会员价
限时优惠
闪购中

尺寸对比

Small:
New
Sale
Plus
Large:
New
Sale
Plus Member

实际应用场景

20% OFF
Hot

AeroFit 2 Earbuds

Plus Member Price

$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)文本大小适用场景
sm24px24px14px紧凑布局、表格、列表
lg24px28px14px / 16px默认大小,产品卡片

响应式行为

  • < 1920px: lg 尺寸高度为 24px,文字 14px
  • ≥ 1920px (lg-desktop): lg 尺寸高度增加到 28px,文字 16px

促销徽章 (variant="promotional") 的内容区域会向右偏移,为左侧的装饰图标留出空间:

  • sm 尺寸:左侧内边距 24px
  • lg 尺寸:左侧内边距 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" 时生效)
childrenReactNode-徽章内容
classNamestring-自定义 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 上反馈

Last updated on