首页
/ Material UI ButtonGroup 实战指南:分组按钮的用法、Props 详解与源码实现解析

Material UI ButtonGroup 实战指南:分组按钮的用法、Props 详解与源码实现解析

2026-09-06 13:30:43作者:董灵辛Dennis

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>
  );
}

有两个要点需要注意:

  1. 按钮必须是直接子元素(immediate children)。这一点由源码保证:ButtonGroup.js 中通过 getValidReactChildren(children) 取出有效子元素后,对每个子元素按下标判断位置——第 0 个加 firstButton 类、最后一个加 lastButton 类、中间的加 middleButton 类,并写入 ButtonGroupButtonContext。如果你的按钮被额外嵌套了一层 Fragment 以外的中间组件,位置类名将无法正确生成,圆角合并与边框合并的样式就会失效。
  2. 建议提供 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 支持全部三种标准变体:textoutlined(默认)、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 控制整体外观

sizecolor 两个 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(紧凑)按钮样式。其值被放入 ButtonGroupContextButtonGroup.js 中的 context 对象),由 Button 内部读取并优先于自身默认值生效。
  • color:默认 'primary',可选 'inherit' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' 或主题中注册的自定义调色板颜色。除下发给按钮外,color 还直接参与根节点的样式计算——useUtilityClasses 会生成 colorPrimary / colorSecondary 等 root 类(ButtonGroup.js),而 text 变体的分隔线颜色也由调色板推导,因此改 color 时整组(含分隔线)都会变色。
  • 此外 disableddisableElevationdisableFocusRippledisableRipplefullWidth 同样通过 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 通过 ButtonGroupforwardRef 透传到根 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 可以直接作为 PopperanchorEl
  • 箭头按钮使用 size="small" 让它在组内更窄——源码中每个分组按钮有 minWidth: 40 的下限(ButtonGroup.js),small 尺寸配合纯图标内容可以让箭头区域保持紧凑;
  • 无障碍属性(aria-controlsaria-expandedaria-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)

Buttonloading 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 还通过 useDefaultPropsname: '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 / middleButtonoverridesResolverButtonGroup.js)明确把 styles.groupedstyles.firstButtonstyles.lastButtonstyles.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 的运作可以概括为三步:

  1. 归一化 propsforwardRef 解构后为 colordisableddisableElevationfullWidthorientationsizevariant 等填充默认值(ButtonGroup.js),ownerState 携带这些归一化值供 styled 变体选择使用;
  2. 下发组级配置ButtonGroupContext.ProviderButtonGroupContext.ts)把 className(grouped)colorsizevariantdisabled 等传给每个子 Button,后者据此获得 MuiButtonGroup-grouped 类并继承组级配置;
  3. 标注按钮位置getButtonPositionClassName(index) 按下标为子元素计算 firstButton / middleButton / lastButton,经 ButtonGroupButtonContextButtonGroupButtonContext.ts)传给子按钮。位置类名正是圆角裁剪与边框合并样式的选择器基础,这也再次印证了"按钮必须是直接子元素"的约束——中间再包一层组件,Button 就读不到位置类名了。

组级根样式则集中在 ButtonGroupRoot 的 variants 列表里(ButtonGroup.js):display: inline-flex + 主题 shape.borderRadius 打底,variant × orientation × color 的笛卡尔组合分别处理阴影、分隔线、边框合并与圆角,最后统一为每个分组按钮设置 minWidth: 40。理解了这条链路,你就知道该在哪里用 sxclasses 或主题 styleOverrides 去定制按钮组的任何一处细节了。

参考文件

登录后查看全文
热门项目推荐
相关项目推荐