首页
/ Material UI Table 组件实战:从基础表格到分页、排序、虚拟滚动与无障碍实现

Material UI Table 组件实战:从基础表格到分页、排序、虚拟滚动与无障碍实现

2026-09-04 19:20:41作者:邵娇湘

本篇技术指南基于 Material UI 官方 Table 组件文档及其配套源码,系统讲解 React 表格组件家族(TableTableHeadTableBodyTableCellTablePaginationTableSortLabel 等)的组成与使用方式,覆盖基础表格、稠密表格、排序与多选、自定义分页、吸顶表头、列分组、可折叠行、跨行跨列、虚拟滚动和无障碍访问等全部核心场景,并结合开源仓库源码说明上下文继承、aria-sort 映射等底层机制,读完后你能独立构建从静态数据展示到高性能大数据表格的完整方案。

一、Table 组件家族总览

Material UI 中的 Table 是一组围绕原生 <table> 语义封装的组件集合,官方文档(table.md)将其设计目标概括为:以易于扫视的方式展示数据,帮助用户发现模式与洞察;表格可以嵌入卡片等主要内容中,并可包含配套可视化、导航以及查询与操作数据的工具。

各组件及其默认渲染的 HTML 元素如下:

组件 默认渲染元素 职责
TableContainer <div> 外层包装,为 Table 提供横向滚动能力
Table <table> 表格主元素
TableHead <thead> 表头行容器
TableBody <tbody> 表体行容器
TableRow <tr> 行,可用于 TableHead / TableBody / TableFooter
TableCell <th><td> 单元格,在 TableHead 内默认渲染 <th>,在 TableBody 内默认渲染 <td>
TableFooter <tfoot> 可选的表尾行容器
TablePagination 基于 TableCell 分页控件
TableSortLabel 行内控件 列头排序控件,支持升序/降序切换

对应的源码目录结构为 packages/mui-material/src/Table(Table 核心实现)、TableCellTablePaginationTableSortLabel 等平级目录,全部由 @mui/material 包导出。

二、基础表格与上下文继承机制

2.1 基础示例

仓库中最简洁的完整示例见 BasicTable.tsx

import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';

function createData(name, calories, fat, carbs, protein) {
  return { name, calories, fat, carbs, protein };
}

const rows = [
  createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
  createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
  createData('Eclair', 262, 16.0, 24, 6.0),
  createData('Cupcake', 305, 3.7, 67, 4.3),
  createData('Gingerbread', 356, 16.0, 49, 3.9),
];

export default function BasicTable() {
  return (
    <TableContainer component={Paper}>
      <Table sx={{ minWidth: 650 }} aria-label="simple table">
        <TableHead>
          <TableRow>
            <TableCell>Dessert (100g serving)</TableCell>
            <TableCell align="right">Calories</TableCell>
            <TableCell align="right">Fat&nbsp;(g)</TableCell>
            <TableCell align="right">Carbs&nbsp;(g)</TableCell>
            <TableCell align="right">Protein&nbsp;(g)</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {rows.map((row) => (
            <TableRow
              key={row.name}
              sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
            >
              <TableCell component="th" scope="row">
                {row.name}
              </TableCell>
              <TableCell align="right">{row.calories}</TableCell>
              <TableCell align="right">{row.fat}</TableCell>
              <TableCell align="right">{row.carbs}</TableCell>
              <TableCell align="right">{row.protein}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

几个关键细节:

  • TableContainer component={Paper} 表示容器根节点渲染为 Paper,从而获得卡片式外观与横向滚动支持;
  • sx={{ minWidth: 650 }} 配合容器形成横向滚动;
  • aria-label="simple table" 为无 <caption> 的表格提供可访问名称;
  • 首列 component="th" scope="row" 将数据列标记为行头,这是官方示例中的无障碍约定(详见第七节)。

2.2 源码解析:Table 如何把 padding/size 下发给单元格

Table 的核心实现在 Table.js。其默认 props 为:

Prop 默认值 说明
component 'table' 根节点元素;若替换为其他组件会自动补 role="table"
padding 'normal' 取值 normal / checkbox / none,被 TableCell 继承
size 'medium' 取值 medium / small(即稠密表格),被 TableCell 继承
stickyHeader false 表头吸顶开关
const table = React.useMemo(
  () => ({ padding, size, stickyHeader }),
  [padding, size, stickyHeader],
);

return (
  <TableContext.Provider value={table}>
    <TableRoot as={component}
      role={component === defaultComponent ? null : 'table'}
      ... />
  </TableContext.Provider>
);

可以看到 Table 通过 TableContext{ padding, size, stickyHeader } 三元组下发给整棵子树,并且当 component 不是 'table' 时自动补充 role="table" 以保持 ARIA 语义。同时 stickyHeader 开启时根节点样式从 border-collapse: collapse 切换为 separate(见 Table.js L43-L50),这是 position: sticky 表头单元格能正确生效的前提。

2.3 源码解析:TableCell 的 th/td 自动切换

TableCell 的实现见 TableCell.js,它同时消费两级上下文:

  • TableContext:来自 Tablepadding / size / stickyHeader
  • Tablelvl2Context:来自 TableHead / TableBody / TableFootervariant'head' / 'body' / 'footer')。

核心逻辑:

const isHeadCell = tablelvl2 && tablelvl2.variant === 'head';

let component;
if (componentProp) {
  component = componentProp;
} else {
  component = isHeadCell ? 'th' : 'td';
}

let scope = scopeProp;
// scope is not a valid attribute for <td/> elements.
if (component === 'td') {
  scope = undefined;
} else if (!scope && isHeadCell) {
  scope = 'col';
}

由此可以确认三件事:

  1. TableCell 放在 TableHead 内自动渲染 <th>,放在 TableBody 内自动渲染 <td>,与官方文档描述一致;
  2. 表头单元格在未显式指定 scope 时自动补 scope="col";而 <td>scope 不是合法属性(HTML 规范),会被主动移除;
  3. paddingsize 若未在单元格上显式指定,会回落到 Table 的上下文值——这解释了为什么稠密表格只需在 Table 上设置一次 size="small" 即可全局生效。

此外 TableCell 还负责无障碍排序语义:传入 sortDirection="asc" | "desc" 时自动映射为 aria-sort="ascending" | "descending"TableCell.js L224-L227),供 TableSortLabel 场景直接使用。单元格样式方面,size="small" 时内边距从 16px 收窄为 6px 16pxpadding="checkbox" 时列宽固定 48px 防止复选框列被撑开(TableCell.js L92-L115)。

三、稠密表格与大数据场景的选型

3.1 Dense table

稠密表格只需将 size 设为 small,即可通过上文所述的上下文继承机制让所有单元格收窄内边距,参考示例 DenseTable.tsx

<Table size="small">
  {/* 其余结构与基础表格一致 */}
</Table>

3.2 何时改用 DataGrid

官方文档明确提示:Table 与原生 <table> 元素近似映射,这一约束使得构建功能丰富的数据表格(筛选、批量编辑、树形结构等)较为吃力;而面向大量表格数据场景的 DataGrid 组件(@mui/x-data-grid)以更有约束力的结构换取更强大的能力。仓库中的演示 DataTable.tsx 即为对照示例:

import { DataGrid, GridCell, GridColDef } from '@mui/x-data-grid';

const columns: GridColDef[] = [
  { field: 'id', headerName: 'ID', width: 70 },
  { field: 'firstName', headerName: 'First name', width: 130 },
  // ...
];

const paginationModel = { page: 0, pageSize: 5 };

function renderRowHeaderCell(props: GridCellProps) {
  return (
    <GridCell
      {...props}
      role={props.column.field === 'fullName' ? 'rowheader' : 'gridcell'}
    />
  );
}

export default function DataTable() {
  return (
    <Paper sx={{ height: 400, width: '100%' }}>
      <DataGrid
        rows={rows}
        columns={columns}
        initialState={{ pagination: { paginationModel } }}
        pageSizeOptions={[5, 10]}
        checkboxSelection
        slots={{ cell: renderRowHeaderCell }}
        sx={{ border: 0 }}
      />
    </Paper>
  );
}

选型结论:结构化、展示型、行数可控的表格用 Table 家族;需要复杂数据处理能力(排序、选择、分页模型、自定义 valueGetter 等)时用 DataGrid。注意 DataGrid 使用 ARIA role 而非原生表格元素,其行头需通过 role="rowheader" 自定义(示例中的 renderRowHeaderCell 即演示了这一点)。

四、排序与选择(EnhancedTable)

官方 "Sorting & selecting" 示例(EnhancedTable.tsx)演示了一个完整的功能表格:Checkbox 行选择、自定义 ToolbarTableSortLabel 列头排序,以及 TablePagination 分页。其中有两条官方文档强调的布局要点:

  1. 表格固定宽度以演示横向滚动:为 Table 设置固定 minWidth 后,内容超宽时由 TableContainer 提供滚动。
  2. 分页控件必须放在表格之外:为防止分页控件随横向滚动被卷走,TablePagination 被放置在 Table 之外(另一示例 CustomPaginationActionsTable.tsx 则展示了把分页放在 TableFooter 内部的写法)。

TableSortLabel 的典型用法(列头点击切换排序方向,order / onSortClick 受控):

<TableCell
  sortDirection={order_by === 'name' ? order : false}
  padding="none"
>
  <TableSortLabel
    active={order_by === 'name'}
    direction={order_by === 'name' ? order : 'asc'}
    onClick={createSortHandler('name')}
  >
    Name
  </TableSortLabel>
</TableCell>

其中 sortDirectionTableCell 源码自动转译为 aria-sort,屏幕阅读器可正确播报当前列的排序状态,无需手动维护 ARIA 属性。

五、TablePagination 分页控件深入

TablePagination 的完整实现见 TablePagination.js,它是一个基于 TableCell 的组件,官方注释建议放在 TableFooter 内使用。关键参数(结合源码逐项核实):

Prop 默认值 说明
count 必填(整数) 总行数;-1 表示服务端分页、总行数未知,此时显示文案变为 "more than N" 且页码范围校验自动跳过
page 必填(整数) 当前页,从 0 开始;开发模式下 page 超出 0 ~ ceil(count/rowsPerPage)-1 会抛出 MUI: The page prop of a TablePagination is out of range 警告
rowsPerPage 必填(整数) 每页行数;-1 表示显示全部行
onPageChange 必填 翻页回调 (event, page) => void
onRowsPerPageChange 每页行数变更回调
rowsPerPageOptions [10, 25, 50, 100] 每页行数下拉选项(见下方两种形式);选项少于 2 个时下拉整体不渲染
ActionsComponent TablePaginationActions 翻页按钮区组件,可完全替换为自定义实现
showFirstButton / showLastButton false 是否显示"首页"/"末页"按钮
colSpan 1000(当根节点为 TableCell/td 时) 使分页行横跨整张表格
labelDisplayedRows (v) => \from{from}–{to} of ${count}`` 自定义"显示行数"文案,接收 { from, to, count, page }
labelRowsPerPage 'Rows per page:' 每页行数标签,支持国际化替换
getItemAriaLabel (type) => \Go to ${type} page`` 翻页按钮的无障碍名称
slots / slotProps {} 针对 roottoolbarspacerselectmenuItemdisplayedRowsactions 等内部槽位的组件与 props 定制

源码中的两处实现细节值得注意(TablePagination.js L189-L202):

let colSpan;
if (component === TableCell || component === 'td') {
  colSpan = colSpanProp || 1000; // col-span over everything
}
  • 放在表格内时分页行自动横跨全部列,视觉上是整行一条分隔带;
  • count === -1getLabelDisplayedRowsTo 返回 (page + 1) * rowsPerPage,文案呈现 "more than N",这正是服务端分页场景的官方支持方式。

5.1 自定义 rowsPerPageOptions

rowsPerPageOptions 接受两种数组元素形式(table.md 原文即给出这两种写法,源码 TablePagination.js L294-L302rowsPerPageOption.label ? ... : rowsPerPageOption 的三元表达式与之对应):

// 形式一:纯数字数组,数字同时作为选项的 label 与 value
<TablePagination rowsPerPageOptions={[10, 50]} />

// 形式二:对象数组,value 为取值、label 为展示文本
// 适合 'All' 这类非数字标签:value=-1 即"显示全部行"
<TablePagination rowsPerPageOptions={[10, 50, { value: -1, label: 'All' }]} />

5.2 自定义分页按钮(ActionsComponent)

通过 ActionsComponent 可整体替换翻页按钮区。官方示例 CustomPaginationActionsTable.tsx 中实现了 TablePaginationActions 组件,将 TablePagination 放入 TableFooter 内部并自定义按钮样式;该组件接收 countpagerowsPerPageonPageChangeshowFirstButtonshowLastButtongetItemAriaLabeldisabled 等 props(见 TablePagination.js L314-L326),因此自定义实现与内置实现在接口上完全兼容,可平滑替换。

六、吸顶表头、列分组、折叠行与跨行列

6.1 Sticky header(吸顶表头)

StickyHeadTable 示例(StickyHeadTable.tsx)的核心结构:

<TableContainer sx={{ maxHeight: 440 }}>
  <Table stickyHeader aria-label="sticky table">
    ...
  </Table>
</TableContainer>

原理(源码证据):

  1. TablestickyHeader 下将根节点样式切为 border-collapse: separateTable.js L43-L50),否则 position: stickycollapse 边框模型下在多数浏览器中不可靠;
  2. TableCell 依据 variant === 'head' && table.stickyHeaderTableCell.js L218)为表头单元格叠加 position: sticky; top: 0; z-index: 2 及背景色(TableCell.js L158-L165),背景色用于遮挡滚动到表头下方的内容。

因此 stickyHeader 必须与一个有高度上限的滚动容器TableContainer sx={{ maxHeight }} 或页面级滚动)配合使用。

6.2 Column grouping(列分组)

通过在一个 TableHead 中渲染多行 TableRow 实现多层列头,配合 colSpan 分组上层标题(ColumnGroupingTable.tsx):

<TableHead>
  <TableRow>
    <TableCell align="center" colSpan={2} sx={{ py: 1 }}>
      Breakfast
    </TableCell>
    <TableCell align="center" colSpan={2} sx={{ py: 1 }}>
      Dinner
    </TableCell>
  </TableRow>
  <TableRow>
    <TableCell sx={{ borderColor: 'divider' }}>Calories</TableCell>
    <TableCell sx={{ borderColor: 'divider' }}>Carbs (g)</TableCell>
    <TableCell sx={{ borderColor: 'divider' }}>Calories</TableCell>
    <TableCell sx={{ borderColor: 'divider' }}>Carbs (g)</TableCell>
  </TableRow>
</TableHead>

6.3 Collapsible table(可折叠行)

CollapsibleTable 示例(CollapsibleTable.tsx)使用 Collapse 组件实现展开更多信息的行:在隐藏列中放一个 IconButton 切换 open 状态,Collapse in={open} 控制一个占满整行(colSpan)的子行高度动画展开/收起。要点是展开行同样是一个 TableRow + 单格 TableCell colSpan={5},内部再嵌套内容块。

6.4 Spanning table(跨行跨列)

SpanningTable 示例(SpanningTable.tsx)演示原生 rowSpan / colSpanTableRow / TableCell 上直接可用,无需额外 API。

6.5 Virtualized table(虚拟滚动)

ReactVirtualizedTable 示例(ReactVirtualizedTable.tsx)展示如何将 react-virtuoso 与 Table 组件结合:官方示例渲染 200 行并可轻松扩展到更大规模,通过只渲染可视区域内的行来解决大数据量下的渲染性能问题。适用前提是数据行高度固定或变化可控,且数据为客户端内存中的数组;若行数巨大且需要服务端加载,优先考虑 DataGrid 或服务端分页(count={-1})。

七、无障碍(Accessibility)

官方文档引用了 WAI 表格教程作为参考依据,并给出两条关键实践。

7.1 行头与列头(Row and column headers)

表头单元格用于标识每行/每列的数据,屏幕阅读器会利用这种关联在导航表格时提供上下文。TableCellTableHead 内自动渲染 <th>scope="col"),在 TableBody 内自动渲染 <td>;当某个表体单元格承载了该行标识信息时,应显式将其渲染为行头:

<TableRow>
  <TableCell component="th" scope="row">
    {row.name}
  </TableCell>
  <TableCell>{row.calories}</TableCell>
</TableRow>

官方建议:行头应选择有语义的值(如人名、产品名)而非任意索引;一行中可以有多个行头单元格(例如同时存在"名"和"姓"两列时)。这与第二节源码中"显式 component 优先于上下文推导"的逻辑(TableCell.js L193-L198)完全一致。

对于 DataGrid,因其使用 ARIA role 而非原生表格元素,行头需通过自定义 cell 渲染 role="rowheader",参见第三节 renderRowHeaderCell 示例。

7.2 Caption

caption 相当于表格的标题:多数屏幕阅读器会播报 caption 内容,帮助用户定位表格、理解其主题并决定是否阅读。AccessibleTable 示例(AccessibleTable.tsx)即为演示。样式上,Table 已内置 caption 的排版(body2 字号、次要文字色、底部对齐),见 Table.js L36-L42

<Table sx={{ minWidth: 650 }}>
  <caption>List of materials</caption>
  ...
</Table>

注意基础示例(BasicTable)未用 caption,因此通过 aria-label 提供等价的可访问名称;两者取其一即可保证屏幕阅读器能识别表格。

八、主题定制与类名

  • 官方 CustomizedTables 示例(CustomizedTables.tsx)演示了通过 sx 与主题 components 覆盖修改表格边框、圆角、单元格背景等外观,可参考定制指南页面了解 components.MuiTable / MuiTableCell 等 styleOverrides 机制;
  • 各组件的 class key(如 MuiTable-rootMuiTableCell-stickyHeaderMuiTableCell-paddingCheckboxMuiTablePagination-actions 等)由 tableClasses.ts / tableCellClasses.ts / tablePaginationClasses.ts 生成,可用于精准 CSS 选择。

九、小结

场景 推荐方案
简单静态数据展示 TableContainer + Table + 头/体结构,aria-label<caption> 提供名称
密集信息 Table size="small"(上下文自动下发至所有单元格)
排序/多选/分页 TableSortLabelaria-sort 自动)+ TablePagination(放表格外防横向滚动,或放入 TableFooter
服务端分页 count={-1} + rowsPerPageOptions{ value: -1, label: 'All' }
长表格 stickyHeader + 限高滚动容器
复杂数据表(编辑、树、分组) 迁移到 @mui/x-data-grid 的 DataGrid
客户端超大数组 react-virtuoso 虚拟滚动示例

所有示例源码均可在 docs/data/material/components/table/ 目录按 BasicTable.tsxDenseTable.tsxEnhancedTable.tsxStickyHeadTable.tsxColumnGroupingTable.tsxCollapsibleTable.tsxSpanningTable.tsxReactVirtualizedTable.tsxCustomPaginationActionsTable.tsxCustomizedTables.tsxAccessibleTable.tsx 查看,组件实现集中在 packages/mui-material/src/Table/TableCell/TablePagination/ 三个目录,便于按本文结论对照阅读。

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