Material UI Badge 组件全解析:计数、圆点、可见性控制与源码实现
【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui
Material UI 的Badge组件用于在其包裹的子元素右上角生成一个小标记,以承载未读消息数、通知计数、在线状态等补充性状态信息。本文以 Badge 官方文档 为主线,完整覆盖其全部用法指南与演示场景,并深入 组件源码 与 单元测试,讲解max截断、showZero隐藏、不可见过渡动画保留等行为的底层实现,读完后可直接在项目中正确、可访问地落地 Badge 的各种形态。
使用指南:官方给出的三条设计准则
官方文档在 badges.md 中明确了三条使用规范,这三条准则同时决定了组件的语义和可访问性设计:
- 徽标只用于补充性状态:Badge 适合展示短小的计数或紧凑状态,例如收件箱按钮上的未读消息数。如果某个状态本身非常重要,应直接展示在界面中,而不是仅依赖徽标传达。
- 徽标语义要并入宿主元素的可访问名称:Badge 是依附于另一元素的视觉提示,它的含义应包含在宿主元素的 accessible name 中。例如应使用
aria-label="Inbox, 4 unread messages",而不是简单的aria-label="Inbox"。 - 圆点徽标只用于简单状态:
variant="dot"不显示文字或数字,只应在周边 UI 已能明确表达状态含义时使用,例如Online或Unread。
官方的 入门演示 就是这三条准则的集中体现:
import IconButton from '@mui/material/IconButton'; import Badge from '@mui/material/Badge'; import MailIcon from '@mui/icons-material/Mail'; const maxVisibleNotifications = 99; const unreadNotificationsCount = 100; function getUnreadNotificationsLabel(count: number) { if (count === 0) { return 'show no unread notifications'; } if (count > maxVisibleNotifications) { return `show more than ${maxVisibleNotifications} unread notifications`; } return `show ${count} unread notification${count === 1 ? '' : 's'}`; } export default function BadgeIntro() { const label = getUnreadNotificationsLabel(unreadNotificationsCount); return ( <IconButton aria-label={label}> <Badge badgeContent={unreadNotificationsCount} color="secondary" max={maxVisibleNotifications} > <MailIcon /> </Badge> </IconButton> ); }注意其中的关键细节:
- 徽标数量(100)超过了
max(99),因此视觉显示为99+,但aria-label中生成的是 "show more than 99 unread notifications",即可访问名称反映真实语义,而不是被截断后的视觉值; aria-label挂在宿主IconButton上而非 Badge 上,因为从源码看,Badge 内部的标记元素默认带有aria-hidden="true"(见下文 源码分析),屏幕阅读器根本不会朗读它。
列表项场景:Badge 挂在 ListItemButton 上
ListItemButton 演示 将同样模式应用到列表中,未读计数被并入列表项的可访问名称:
<ListItemButton selected aria-current="page" aria-label={`Inbox, ${unreadMessagesCount} unread messages`} > <ListItemIcon> <Badge badgeContent={unreadMessagesCount} color="primary"> <InboxIcon /> </Badge> </ListItemIcon> <ListItemText primary="Inbox" /> </ListItemButton>可见的计数(4)通过模板字符串直接包含在aria-label里,使其在周边 UI 的语境中被朗读。
Badge 内容:badgeContent
使用badgeContent属性在包裹元素上添加短计数或标签。最简示例来自 SimpleBadge:
<IconButton aria-label="show 4 unread messages"> <Badge badgeContent={4} color="primary"> <MailIcon /> </Badge> </IconButton>badgeContent的类型是React.ReactNode(见 类型定义),因此不仅可以传数字,也可以传字符串或任意节点,如"NEW"。
圆点徽标(Dot badge)
使用variant="dot"得到一个不带计数的紧凑状态指示器。DotBadge 演示:
<IconButton aria-label="show new notifications"> <Badge color="secondary" variant="dot"> <NotificationsIcon /> </Badge> </IconButton>从源码看,variant="dot"会走独立的样式变体:徽标高度、最小宽度收窄为 8px(RADIUS_DOT = 4,height/minWidth均为其两倍),内边距归零,见 Badge.js 中的变体定义。同时displayValue会被置为undefined,即dot 变体永远不渲染badgeContent——这一点被 测试用例 "should not render badgeContent when variant='dot'" 明确验证。
可见性:invisible 与 showZero
通过invisible属性可以控制徽标的显隐。BadgeVisibility 演示 用一个 Switch 切换invisible:
const [invisible, setInvisible] = React.useState(false); <IconButton aria-label={ invisible ? 'open inbox' : `open inbox, ${unreadMessagesCount} unread messages` } > <Badge color="secondary" badgeContent={unreadMessagesCount} invisible={invisible}> <MailIcon /> </Badge> </IconButton>值得注意:切换可见性时aria-label也随之变化——徽标隐藏后,可访问名称里不再声称有 4 条未读消息,保持语音播报与视觉一致。
零值自动隐藏:当badgeContent为 0 时,徽标默认自动隐藏;若"0"对界面有意义,可以用showZero属性覆盖此行为。ShowZeroBadge 演示 并排对比了两种写法:
<IconButton aria-label="show no unread messages"> <Badge color="secondary" badgeContent={0}> <MailIcon /> </Badge> </IconButton> <IconButton aria-label="show 0 unread messages"> <Badge color="secondary" badgeContent={0} showZero> <MailIcon /> </Badge> </IconButton>第一个徽标不可见,第二个显示 "0"。该逻辑的源码实现位于 useBadge.ts:
let invisible = invisibleProp; if (invisibleProp === false && badgeContentProp === 0 && !showZero) { invisible = true; }也就是说,零值隐藏与invisible是"或"关系:只要badgeContent === 0且未显式开启showZero,徽标就进入不可见状态。测试套件 对这一组合做了穷举验证,包括badgeContent={0}时默认带invisible类、badgeContent={0} showZero时不带invisible类两种断言。
最大值:max
使用max属性为大数字封顶。BadgeMax 演示:
<IconButton aria-label="show 99 unread messages"> <Badge color="secondary" badgeContent={99}> <MailIcon /> </Badge> </IconButton> <IconButton aria-label="show more than 99 unread messages"> <Badge color="secondary" badgeContent={100}> <MailIcon /> </Badge> </IconButton> <IconButton aria-label="show more than 999 unread messages"> <Badge color="secondary" badgeContent={1000} max={999}> <MailIcon /> </Badge> </IconButton>三个徽标分别显示99、99+、999+。截断逻辑在 useBadge.ts 中只有一行核心判断:
const displayValue: React.ReactNode = badgeContent && Number(badgeContent) > max ? `${max}+` : badgeContent;测试用例 精确界定了边界行为:
- 默认
max为 99,badgeContent={100}显示99+; badgeContent={1000} max={999}显示999+;- 等于 max 时不截断:
badgeContent={1000} max={1000}显示1000; - 小于 max 时原样显示:
badgeContent={50} max={1000}显示50。
自定义
颜色(color)
使用color属性把主题调色板颜色应用到徽标上。ColorBadge 演示 展示了三种典型语义配色:
<Badge badgeContent={8} color="primary">...</Badge> // 普通信息 <Badge badgeContent={2} color="success">...</Badge> // 已确认 <Badge badgeContent={1} color="error">...</Badge> // 严重告警color的合法内置取值为default、primary、secondary、error、info、success、warning(见 PropTypes 定义),默认值为'default'。从源码结构看,徽标背景色的样式变体是动态生成的:Badge.js 遍历theme.palette的所有调色板项(过滤掉contrastText),为每个颜色生成backgroundColor: palette[color].main与color: palette[color].contrastText。因此只要你在主题中新增了自定义颜色(例如brand),color="brand"会自动获得对应样式,无需手写 CSS——这一点也被OverridableStringUnion类型(Badge.d.ts)在类型层面支持。
对齐(anchorOrigin)
使用anchorOrigin属性把徽标移动到被包裹元素的任意一角。BadgeAlignment 演示 是一个可交互示例,通过两组单选按钮切换vertical(top/bottom)与horizontal(right/left),并实时渲染对应的 JSX:
<IconButton aria-label="show 12 unread messages"> <Badge badgeContent={12} color="secondary" anchorOrigin={{ vertical: 'bottom', horizontal: 'left', }} > <MailIcon /> </Badge> </IconButton>anchorOrigin的默认值为{ vertical: 'top', horizontal: 'right' }(右上角)。从源码看,两个方向都可以只传一半:getAnchorOrigin 会对缺失的维度单独回退默认值——vertical缺省为top,horizontal缺省为right。例如anchorOrigin={{ vertical: 'bottom' }}等价于bottom+right组合,测试 分别断言了这两种单维写法生成的类名(anchorOriginBottomRightRectangular、anchorOriginTopLeftRectangular)。
定位本身是通过 CSS 自定义变量注入的,见 Badge.js:
const offset = overlap === 'circular' ? '14%' : '0'; const top = vertical === 'top' ? offset : 'auto'; const bottom = vertical === 'bottom' ? offset : 'auto'; const right = horizontal === 'right' ? offset : 'auto'; const left = horizontal === 'left' ? offset : 'auto'; // ... style: { '--Badge-translate': `${horizontal === 'right' ? '50%' : '-50%'}, ${vertical === 'top' ? '-50%' : '50%'}`, '--Badge-inset': `${top} ${right} ${bottom} ${left}`, '--Badge-origin': `${horizontal === 'right' ? '100%' : '0%'} ${vertical === 'top' ? '0%' : '100%'}`, }徽标样式侧(Badge.js)消费这些变量:inset: var(--Badge-inset)决定四边贴边距离,translate(var(--Badge-translate))让徽标始终骑跨在元素边缘(水平方向平移 50%、垂直方向平移 ±50%),transform-origin则指向被包裹元素的对应角,保证缩放动画从正确的角点发生。
重叠形状(overlap)
当被包裹元素是圆形(如 Avatar)时,使用overlap="circular"让徽标更贴近弧线。BadgeOverlap 演示 对比了矩形与圆形两种宿主:
<Badge color="secondary" badgeContent={1}> <Rectangle /> </Badge> <Badge color="secondary" overlap="circular" badgeContent={1}> <Circle /> </Badge>overlap的默认值为'rectangular'。从源码看它只影响一个值:定位 offset 从0(矩形,徽标紧贴直角边缘)变为14%(圆形,徽标内收以匹配圆弧,见 Badge.js L232)。
自定义样式
官方文档指出,可以通过主题样式覆盖(theme style overrides)、sx属性或styled()三种途径自定义徽标,并建议参考项目的自定义化指南。CustomizedBadges 演示 展示了用styled()制作"在线/离线"状态点的完整实现,这是列表头像状态指示的经典写法:
const ContactStatusBadge = styled(Badge, { shouldForwardProp: (prop) => prop !== 'status', })<{ status: ContactStatus }>(({ theme, status }) => { const themePalette = (theme.vars ?? theme).palette; const offlineBadgeColor = theme.vars ? theme.vars.palette.Avatar.defaultBg : theme.palette.grey[400]; return { '& .MuiBadge-badge': { height: 10, minWidth: 10, border: `1px solid ${themePalette.grey[300]}`, boxShadow: `0 0 0 2px ${themePalette.background.paper}`, ...(status === 'offline' && { backgroundColor: offlineBadgeColor, ...(theme.vars ? {} : theme.applyStyles('dark', { backgroundColor: theme.palette.grey[600], })), }), }, }; });使用时的要点:
<ContactStatusBadge status={contact.status} color={contact.status === 'online' ? 'success' : 'default'} variant="dot" overlap="circular" anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} > <Avatar>{contact.initials}</Avatar> </ContactStatusBadge>shouldForwardProp: (prop) => prop !== 'status'用于阻止自定义的status属性泄漏到 DOM;border: 1px solid grey[300]+boxShadow: 0 0 0 2px background.paper组合出了圆点与头像之间的"描边间隔"效果,而不是把圆点缩小;- 离线状态色通过
theme.vars分支兼容了 CSS 变量主题(palette.Avatar.defaultBg)与静态主题(grey[400])两套取值路径。
此外,Badge 还暴露了slots/slotProps两个插槽机制,可分别替换root与badge两个内部节点的组件类型和属性,测试 验证了自定义槽组件的渲染,以及通过slotProps={{ badge: { 'aria-hidden': false, 'aria-label': '10 notifications' } }}覆盖徽标节点默认可访问性属性的能力。
源码深入:组件结构与隐藏机制
双节点结构与 aria-hidden
从 Badge.js 的渲染输出看,Badge 是一个"外壳 + 标记"的双节点结构:
<RootSlot {...rootProps}> {children} <BadgeSlot {...badgeProps}>{displayValue}</BadgeSlot> </RootSlot>- root 节点(
BadgeRoot):position: relative的行内 flexspan,为绝对定位的徽标提供坐标参考,同时透传component、className及所有其他宿主属性; - badge 节点(
BadgeBadge):绝对定位的标记本身,硬编码aria-hidden: true(Badge.js L243),并渲染displayValue。
测试 hides the visual badge from assistive technologies by default 直接断言了该节点带有aria-hidden="true"属性。这正是官方使用指南第二条"把徽标语义并入宿主元素 accessible name"的结构性原因:徽标本体对辅助技术不可见,若宿主元素不携带计数信息,屏幕阅读器用户将完全丢失该状态。
invisible 过渡动画的"状态保留"机制
invisible徽标并不是从 DOM 中移除,而是通过transform: scale(0)收缩消失(Badge.js L124-L133),配合主题过渡曲线实现缩放动画。这里有一个容易被忽略的细节:徽标进入不可见状态的那一帧,color、variant、anchorOrigin、overlap、badgeContent可能同时变化(例如未读数归零时从standard切回dot),如果直接用新值渲染,消失动画就会"变形"——从一个新位置的圆形闪到旧位置。
源码的解决方案在 Badge.js:用usePreviousProps缓存上一帧的这五个属性,当徽标处于不可见状态时沿用上一帧的值渲染:
const invisible = invisibleFromHook || (badgeContent == null && variantProp !== 'dot'); // ... const { color = colorProp, overlap = overlapProp, anchorOrigin: anchorOriginPropProp, variant = variantProp, } = invisible ? prevProps : props;测试 "retains anchorOrigin, content, color, max, overlap and variant when invisible is true for consistent disappearing transition" 专门验证了这一点:从secondary色、dot变体切换为 0 计数、primary色、standard变体、bottom-left锚点时,徽标仍然保留colorSecondary、dot、anchorOriginTopRightRectangular类名,即以旧形态平滑缩小。
另外注意 invisible 的触发条件是invisibleFromHook || (badgeContent == null && variantProp !== 'dot'):未传badgeContent的 standard 徽标默认不可见,而variant="dot"即使无内容也保持可见(对应测试 L134-L144 中badgeContent={undefined} variant="dot"不带invisible类的断言)。
类名体系
badgeClasses.ts 通过generateUtilityClasses('MuiBadge', [...])生成了完整的工具类名集,前缀为MuiBadge-,主要包括:
| 类别 | 类名示例 | 触发条件 |
|---|---|---|
| 结构 | root、badge | 始终存在 |
| 变体 | standard、dot | variant |
| 状态 | invisible | 不可见 |
| 颜色 | colorPrimary、colorSecondary、colorError、colorInfo、colorSuccess、colorWarning | color !== 'default' |
| 锚点 | anchorOriginTopRight、anchorOriginBottomLeft等四角组合 | anchorOrigin |
| 重叠 | overlapRectangular、overlapCircular | overlap |
| 锚点 × 重叠 | anchorOriginTopRightCircular等八个组合 | 联合定位 |
这些类名可用于classes属性覆盖、styled选择器(如演示中的'& .MuiBadge-badge')或主题components.MuiBadge.styleOverrides。需要留意源码中的一处标注:badgeClasses.ts L81 带有TODO: v6 remove the overlap value from these class keys,即anchorOriginTopRightCircular这类"锚点 + 重叠"的组合类名属于历史遗留,未来大版本中组合定位样式将改由overlapCircular与锚点类名组合承担——当前版本使用它们没有问题,但升级时需留意。
Props 速查表
综合 Badge.d.ts 与 PropTypes:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
anchorOrigin | { vertical?: 'top' \| 'bottom'; horizontal?: 'left' \| 'right' } | { vertical: 'top', horizontal: 'right' } | 徽标锚点,缺省维度单独回退 |
badgeContent | React.ReactNode | — | 徽标内显示的内容 |
children | React.ReactNode | — | 徽标所依附的宿主元素 |
color | 'default' \| 'primary' \| 'secondary' \| 'error' \| 'info' \| 'success' \| 'warning' \| string | 'default' | 主题调色板颜色,支持自定义主题色 |
invisible | boolean | false | 强制隐藏徽标 |
max | number | 99 | 显示上限,超过时渲染max+ |
overlap | 'rectangular' \| 'circular' | 'rectangular' | 宿主形状,圆形时徽标内收 14% |
showZero | boolean | false | 控制badgeContent为 0 时是否隐藏 |
variant | 'standard' \| 'dot' | 'standard' | 标准计数或圆点 |
component | React.ElementType | span | 替换根节点元素 |
slots/slotProps | 见 BadgeSlots | {} | 替换/配置root与badge插槽 |
sx | SxProps<Theme> | — | 系统属性与内联样式 |
classes | Partial<BadgeClasses> | — | 类名覆盖 |
小结与延伸阅读
Badge的行为可以归纳为四条规则链:badgeContent决定显示什么,max决定显示上限(Number(badgeContent) > max时渲染${max}+),showZero/invisible/空内容三者决定显隐,anchorOrigin×overlap通过 CSS 变量决定贴边位置。所有视觉变化都以scale过渡呈现,并在隐藏瞬间保留上一帧的形态参数,保证了消失动画的一致性。
如需继续深入,可从以下仓库文件入手:
- 组件实现:packages/mui-material/src/Badge/Badge.js
- 逻辑 Hook:packages/mui-material/src/Badge/useBadge.ts
- 类型定义:packages/mui-material/src/Badge/Badge.d.ts
- 测试:packages/mui-material/src/Badge/Badge.test.js
- 全部官方演示源码:docs/data/material/components/badges/
【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考