Material UI ButtonGroup 实战指南:分组按钮的用法、Props 详解与源码实现解析
Material UI 的 ButtonGroup 组件用于将若干相关按钮合并为一个视觉上连体的"按钮组",常用于工具栏、编辑器、合并策略选择等交互密集的场景。本篇基于仓库中官方组件文档 docs/data/material/components/button-group/button-group.md 及其配套示例,完整覆盖官方文档的七类用法(基础用法、变体、尺寸颜色、垂直方向、分割按钮、禁用高程、加载状态),并结合 ButtonGroup 源码 深入讲解其默认值、Context 注入机制与"首/中/尾按钮"类名的生成原理,帮助你既能直接复制代码落地,又能在自定义样式(sx/classes)时准确命中内部类名。
基本用法:用 ButtonGroup 包裹直接子按钮
最基础的按钮组,只需把 Button 组件用 ButtonGroup 包起来即可。文档中的示例如下(对应仓库示例文件 BasicButtonGroup.tsx):
import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup';
export default function BasicButtonGroup() {
return (
<ButtonGroup variant="contained" aria-label="Basic button group">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
);
}
有两个要点需要注意:
- 按钮必须是直接子元素(immediate children)。这一点由源码保证:ButtonGroup.js 中通过
getValidReactChildren(children)取出有效子元素后,对每个子元素按下标判断位置——第 0 个加firstButton类、最后一个加lastButton类、中间的加middleButton类,并写入ButtonGroupButtonContext。如果你的按钮被额外嵌套了一层Fragment以外的中间组件,位置类名将无法正确生成,圆角合并与边框合并的样式就会失效。 - 建议提供
aria-label。渲染层面,ButtonGroup的根节点是role="group"的div(见 ButtonGroup.js 中的<ButtonGroupRoot as={component} role="group">),屏幕阅读器会将其识别为一个按钮分组,aria-label能让分组语义更完整。
渲染结构上,每个子按钮都被两层 Context 包裹:ButtonGroupContext 提供组级共享属性(variant、size、color 等),ButtonGroupButtonContext 提供该按钮的位置类名(见 ButtonGroup.js)。这是 Button 能自动吸收组级配置的关键。
按钮变体(variant)
ButtonGroup 支持全部三种标准变体:text、outlined(默认)、contained。官方示例 VariantButtonGroup.tsx 展示了 outlined 与 text 两种:
<ButtonGroup variant="outlined" aria-label="Basic button group">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
<ButtonGroup variant="text" aria-label="Basic button group">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
不同变体在组内的视觉处理差异很大,这些差异全部写在根组件的 styled variants 中(ButtonGroup.js):
contained:整组只保留外层一处阴影(boxShadow: theme.shadows[2]),组内每个按钮的阴影被移除,中间按钮之间用1px solid grey[400]的竖/横分割线分隔;outlined:相邻按钮的共享边框被设置为透明(borderRightColor: 'transparent',垂直方向对应borderBottomColor),后一个按钮以marginLeft: -1(或marginTop: -1)抵消 1px 缝隙,实现边框无缝合并;悬停时恢复为currentColor以保留 hover 反馈;text:中间按钮右侧(或下方)绘制 1px 分隔线,透明度为onBackground的 23%(深色模式下自动切换为白色 23%),且会按theme.palette中的每个颜色自动生成对应变体,使分隔线颜色与color属性联动(borderColor: theme.alpha(palette[color].main, 0.5))。
因此同一个组内不要混用变体:组级 variant 通过 Context 下发,子 Button 若自行指定 variant 会与组的分隔线逻辑冲突。
size 与 color 控制整体外观
size 和 color 两个 prop 定义在 ButtonGroup 上后会透传给组内所有按钮。官方示例 GroupSizesColors.tsx:
const buttons = [
<Button key="one">One</Button>,
<Button key="two">Two</Button>,
<Button key="three">Three</Button>,
];
<ButtonGroup size="small" aria-label="Small button group">{buttons}</ButtonGroup>
<ButtonGroup color="secondary" aria-label="Medium-sized button group">{buttons}</ButtonGroup>
<ButtonGroup size="large" aria-label="Large button group">{buttons}</ButtonGroup>
这两个属性在源码中的默认值与实现路径如下:
size:默认'medium',可选'small' | 'medium' | 'large'。small对应 dense(紧凑)按钮样式。其值被放入ButtonGroupContext(ButtonGroup.js 中的context对象),由Button内部读取并优先于自身默认值生效。color:默认'primary',可选'inherit' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning'或主题中注册的自定义调色板颜色。除下发给按钮外,color还直接参与根节点的样式计算——useUtilityClasses会生成colorPrimary/colorSecondary等 root 类(ButtonGroup.js),而 text 变体的分隔线颜色也由调色板推导,因此改color时整组(含分隔线)都会变色。- 此外
disabled、disableElevation、disableFocusRipple、disableRipple、fullWidth同样通过 Context 下发,可以一次性禁用整组按钮的涟漪、焦点波纹或禁用整组交互。
垂直方向的按钮组
设置 orientation="vertical" 后,按钮从横排变为纵排。官方示例 GroupOrientation.tsx 同时展示了 outlined、contained、text 三种变体在垂直方向下的效果:
<ButtonGroup orientation="vertical" aria-label="Vertical button group">
{buttons}
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="contained" aria-label="Vertical button group">
{buttons}
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="text" aria-label="Vertical button group">
{buttons}
</ButtonGroup>
从源码看(ButtonGroup.js),orientation 的默认值是 'horizontal',垂直方向的实现要点是:
- 根节点由默认的横排 flex 切换为
flexDirection: 'column'; - 圆角逻辑随之旋转:首/中间按钮的下侧圆角清零、中间/末位按钮的上侧圆角清零,保证组仍呈现为一个整块的圆角矩形(横向时则是首/中间按钮右侧圆角清零、末位/中间按钮左侧圆角清零);
- 变体分隔逻辑整体旋转 90°:outlined 变体在垂直方向改为
borderBottomColor: 'transparent'+marginTop: -1合并边框。
分割按钮(Split Button)
ButtonGroup 还有一个进阶用法:把"主操作按钮"和"下拉箭头按钮"组成一个分割按钮。下拉菜单既用来切换主按钮的动作(官方示例的场景),也可以直接触发一个关联操作。
官方完整示例(SplitButton.tsx):
import * as React from 'react';
import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Grow from '@mui/material/Grow';
import Paper from '@mui/material/Paper';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
const options = ['Create a merge commit', 'Squash and merge', 'Rebase and merge'];
export default function SplitButton() {
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef<HTMLDivElement>(null);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const handleClick = () => {
console.info(`You clicked ${options[selectedIndex]}`);
};
const handleMenuItemClick = (
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
index: number,
) => {
setSelectedIndex(index);
setOpen(false);
};
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event: Event) => {
if (
anchorRef.current &&
anchorRef.current.contains(event.target as HTMLElement)
) {
return;
}
setOpen(false);
};
return (
<React.Fragment>
<ButtonGroup
variant="contained"
ref={anchorRef}
aria-label="Button group with a nested menu"
>
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
<Button
size="small"
aria-controls={open ? 'split-button-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-label="select merge strategy"
aria-haspopup="menu"
onClick={handleToggle}
>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper
sx={{ zIndex: 1 }}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" autoFocusItem>
{options.map((option, index) => (
<MenuItem
key={option}
disabled={index === 2}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</React.Fragment>
);
}
实现细节值得注意的有四点:
ref通过ButtonGroup的forwardRef透传到根div([ButtonGroup.js](https://gitcode.com/GitHub_Trending/ma/material-ui/blob/95f68f3eb2fc42dcccf1ba2a0c4c956b0f22e452/packages/mui-material/src/ButtonGroup/ButtonGroup.js?utm_source=gitcode_repo_files#L249-L250, L327-L335)),因此anchorRef.current可以直接作为Popper的anchorEl;- 箭头按钮使用
size="small"让它在组内更窄——源码中每个分组按钮有minWidth: 40的下限(ButtonGroup.js),small 尺寸配合纯图标内容可以让箭头区域保持紧凑; - 无障碍属性(
aria-controls、aria-expanded、aria-haspopup="menu")打在箭头Button上,让辅助技术能感知下拉状态; - 关闭菜单依赖
ClickAwayListener,且通过anchorRef.current.contains(event.target)排除掉点击在按钮组内部的误关闭。
移除高程(disableElevation)
contained 变体的按钮组自带 shadows[2] 阴影。若你希望整组完全扁平(例如放在工具栏中),使用 disableElevation:
<ButtonGroup disableElevation variant="contained" aria-label="Disabled button group">
<Button>One</Button>
<Button>Two</Button>
</ButtonGroup>
(对应示例 DisableElevation.tsx。)
源码层面,disableElevation 触发一个 styled variant:根节点 boxShadow: 'none'(ButtonGroup.js),同时在 overridesResolver 中追加 styles.disableElevation(主题侧可通过 components: { MuiButtonGroup: { styleOverrides: { disableElevation: ... } } } 覆盖),并在 root 类名中追加 disableElevation。它同样通过 Context 下发给子按钮,避免组内按钮各自再冒出阴影。
加载状态(Loading)
Button 的 loading prop 可以让组内某个按钮进入加载态并禁用交互,其余按钮不受影响。官方示例 LoadingButtonGroup.tsx:
<ButtonGroup variant="outlined" aria-label="Loading button group">
<Button>Submit</Button>
<Button>Fetch data</Button>
<Button loading loadingPosition="start" startIcon={<SaveIcon />}>
Save
</Button>
</ButtonGroup>
loading 属于 Button 自身的 prop 而非 ButtonGroup 的,因此只在需要的那个按钮上声明;loadingPosition="start" 控制加载指示器位于文字前侧。
ButtonGroup Props 完整参考
结合 ButtonGroup.d.ts 中的类型定义,完整 Props 如下(默认值均已在源码中逐一确认):
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
children |
React.ReactNode |
— | 组内按钮,须为直接子元素 |
color |
'inherit' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' |
'primary' |
整组颜色,支持主题自定义调色板颜色 |
component |
React.ElementType |
'div' |
根节点渲染的组件 |
disabled |
boolean |
false |
整组禁用 |
disableElevation |
boolean |
false |
移除阴影 |
disableFocusRipple |
boolean |
false |
禁用键盘焦点涟漪 |
disableRipple |
boolean |
false |
禁用涟漪效果 |
fullWidth |
boolean |
false |
按钮组占满容器宽度(根节点 width: 100%) |
orientation |
'horizontal' | 'vertical' |
'horizontal' |
布局方向 |
size |
'small' | 'medium' | 'large' |
'medium' |
整组按钮尺寸,small 即 dense 样式 |
variant |
'text' | 'outlined' | 'contained' |
'outlined' |
整组变体 |
classes |
Partial<ButtonGroupClasses> |
— | 覆盖内部类名 |
sx |
SxProps<Theme> |
— | 系统样式 |
除上述外,ButtonGroup 还通过 useDefaultProps(name: 'MuiButtonGroup',见 ButtonGroup.js)支持主题级别的 defaultProps 配置——你可以用 DefaultPropsProvider / theme.components.MuiButtonGroup.defaultProps 为应用内所有按钮组统一设置默认变体、尺寸或颜色,而无需逐个传参。
内部类名(classes)与自定义样式锚点
buttonGroupClasses.ts 定义了 MuiButtonGroup 的全部类名键,配合 generateUtilityClass('MuiButtonGroup', slot) 生成形如 MuiButtonGroup-root 的类名:
| 类名键 | 生成类名 | 应用位置 |
|---|---|---|
root |
MuiButtonGroup-root |
根元素 |
contained / outlined / text |
MuiButtonGroup-contained 等 |
根元素(随 variant) |
disableElevation |
MuiButtonGroup-disableElevation |
根元素(随 disableElevation) |
disabled |
MuiButtonGroup-disabled |
子按钮(随 disabled) |
firstButton |
MuiButtonGroup-firstButton |
第一个按钮 |
lastButton |
MuiButtonGroup-lastButton |
最后一个按钮 |
middleButton |
MuiButtonGroup-middleButton |
中间按钮 |
fullWidth |
MuiButtonGroup-fullWidth |
根元素(随 fullWidth) |
horizontal / vertical |
MuiButtonGroup-horizontal / MuiButtonGroup-vertical |
根元素(随 orientation) |
colorPrimary / colorSecondary |
MuiButtonGroup-colorPrimary 等 |
根元素(随 color,类名首字母大写化) |
grouped |
MuiButtonGroup-grouped |
组内每个按钮 |
自定义样式时最常用的锚点就是 grouped / firstButton / lastButton / middleButton。overridesResolver(ButtonGroup.js)明确把 styles.grouped、styles.firstButton、styles.lastButton、styles.middleButton 映射为选择器 & .MuiButtonGroup-xxx——也就是说在主题 styleOverrides 中,grouped 等键会作用到子按钮而非根元素。例如:
<ButtonGroup
variant="contained"
classes={{ root: 'my-group' }}
sx={{
'& .MuiButtonGroup-firstButton': { fontSize: 16 },
'& .MuiButtonGroup-grouped': { minWidth: 56 },
}}
>
<Button>One</Button>
<Button>Two</Button>
</ButtonGroup>
另一个实用细节:源码在根样式中对焦点态做了提升——& .MuiButtonGroup-grouped.MuiButton-focusVisible { zIndex: 1 }(ButtonGroup.js),让获得键盘焦点的按钮渲染在兄弟按钮之上,避免焦点环被相邻按钮的圆角边缘遮挡。
工作机制小结:两层 Context 与位置类名
把源码调用链串起来,ButtonGroup 的运作可以概括为三步:
- 归一化 props:
forwardRef解构后为color、disabled、disableElevation、fullWidth、orientation、size、variant等填充默认值(ButtonGroup.js),ownerState携带这些归一化值供 styled 变体选择使用; - 下发组级配置:
ButtonGroupContext.Provider(ButtonGroupContext.ts)把className(grouped)、color、size、variant、disabled等传给每个子Button,后者据此获得MuiButtonGroup-grouped类并继承组级配置; - 标注按钮位置:
getButtonPositionClassName(index)按下标为子元素计算firstButton/middleButton/lastButton,经ButtonGroupButtonContext(ButtonGroupButtonContext.ts)传给子按钮。位置类名正是圆角裁剪与边框合并样式的选择器基础,这也再次印证了"按钮必须是直接子元素"的约束——中间再包一层组件,Button就读不到位置类名了。
组级根样式则集中在 ButtonGroupRoot 的 variants 列表里(ButtonGroup.js):display: inline-flex + 主题 shape.borderRadius 打底,variant × orientation × color 的笛卡尔组合分别处理阴影、分隔线、边框合并与圆角,最后统一为每个分组按钮设置 minWidth: 40。理解了这条链路,你就知道该在哪里用 sx、classes 或主题 styleOverrides 去定制按钮组的任何一处细节了。
参考文件
- 官方文档:docs/data/material/components/button-group/button-group.md
- 示例代码:BasicButtonGroup.tsx、VariantButtonGroup.tsx、GroupSizesColors.tsx、GroupOrientation.tsx、SplitButton.tsx、DisableElevation.tsx、LoadingButtonGroup.tsx
- 组件源码:ButtonGroup.js、ButtonGroup.d.ts、buttonGroupClasses.ts、ButtonGroupContext.ts、ButtonGroupButtonContext.ts
- 测试:ButtonGroup.test.js
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00