Material UI Table 组件实战:从基础表格到分页、排序、虚拟滚动与无障碍实现
本篇技术指南基于 Material UI 官方 Table 组件文档及其配套源码,系统讲解 React 表格组件家族(Table、TableHead、TableBody、TableCell、TablePagination、TableSortLabel 等)的组成与使用方式,覆盖基础表格、稠密表格、排序与多选、自定义分页、吸顶表头、列分组、可折叠行、跨行跨列、虚拟滚动和无障碍访问等全部核心场景,并结合开源仓库源码说明上下文继承、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 核心实现)、TableCell、TablePagination、TableSortLabel 等平级目录,全部由 @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 (g)</TableCell>
<TableCell align="right">Carbs (g)</TableCell>
<TableCell align="right">Protein (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:来自Table的padding/size/stickyHeader;Tablelvl2Context:来自TableHead/TableBody/TableFooter的variant('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';
}
由此可以确认三件事:
TableCell放在TableHead内自动渲染<th>,放在TableBody内自动渲染<td>,与官方文档描述一致;- 表头单元格在未显式指定
scope时自动补scope="col";而<td>上scope不是合法属性(HTML 规范),会被主动移除; padding和size若未在单元格上显式指定,会回落到Table的上下文值——这解释了为什么稠密表格只需在Table上设置一次size="small"即可全局生效。
此外 TableCell 还负责无障碍排序语义:传入 sortDirection="asc" | "desc" 时自动映射为 aria-sort="ascending" | "descending"(TableCell.js L224-L227),供 TableSortLabel 场景直接使用。单元格样式方面,size="small" 时内边距从 16px 收窄为 6px 16px,padding="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 行选择、自定义 Toolbar、TableSortLabel 列头排序,以及 TablePagination 分页。其中有两条官方文档强调的布局要点:
- 表格固定宽度以演示横向滚动:为
Table设置固定minWidth后,内容超宽时由TableContainer提供滚动。 - 分页控件必须放在表格之外:为防止分页控件随横向滚动被卷走,
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>
其中 sortDirection 由 TableCell 源码自动转译为 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) => \{to} of ${count}`` |
自定义"显示行数"文案,接收 { from, to, count, page } |
labelRowsPerPage |
'Rows per page:' |
每页行数标签,支持国际化替换 |
getItemAriaLabel |
(type) => \Go to ${type} page`` |
翻页按钮的无障碍名称 |
slots / slotProps |
{} |
针对 root、toolbar、spacer、select、menuItem、displayedRows、actions 等内部槽位的组件与 props 定制 |
源码中的两处实现细节值得注意(TablePagination.js L189-L202):
let colSpan;
if (component === TableCell || component === 'td') {
colSpan = colSpanProp || 1000; // col-span over everything
}
- 放在表格内时分页行自动横跨全部列,视觉上是整行一条分隔带;
count === -1时getLabelDisplayedRowsTo返回(page + 1) * rowsPerPage,文案呈现 "more than N",这正是服务端分页场景的官方支持方式。
5.1 自定义 rowsPerPageOptions
rowsPerPageOptions 接受两种数组元素形式(table.md 原文即给出这两种写法,源码 TablePagination.js L294-L302 中 rowsPerPageOption.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 内部并自定义按钮样式;该组件接收 count、page、rowsPerPage、onPageChange、showFirstButton、showLastButton、getItemAriaLabel、disabled 等 props(见 TablePagination.js L314-L326),因此自定义实现与内置实现在接口上完全兼容,可平滑替换。
六、吸顶表头、列分组、折叠行与跨行列
6.1 Sticky header(吸顶表头)
StickyHeadTable 示例(StickyHeadTable.tsx)的核心结构:
<TableContainer sx={{ maxHeight: 440 }}>
<Table stickyHeader aria-label="sticky table">
...
</Table>
</TableContainer>
原理(源码证据):
Table在stickyHeader下将根节点样式切为border-collapse: separate(Table.js L43-L50),否则position: sticky在collapse边框模型下在多数浏览器中不可靠;TableCell依据variant === 'head' && table.stickyHeader(TableCell.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 / colSpan 在 TableRow / TableCell 上直接可用,无需额外 API。
6.5 Virtualized table(虚拟滚动)
ReactVirtualizedTable 示例(ReactVirtualizedTable.tsx)展示如何将 react-virtuoso 与 Table 组件结合:官方示例渲染 200 行并可轻松扩展到更大规模,通过只渲染可视区域内的行来解决大数据量下的渲染性能问题。适用前提是数据行高度固定或变化可控,且数据为客户端内存中的数组;若行数巨大且需要服务端加载,优先考虑 DataGrid 或服务端分页(count={-1})。
七、无障碍(Accessibility)
官方文档引用了 WAI 表格教程作为参考依据,并给出两条关键实践。
7.1 行头与列头(Row and column headers)
表头单元格用于标识每行/每列的数据,屏幕阅读器会利用这种关联在导航表格时提供上下文。TableCell 在 TableHead 内自动渲染 <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-root、MuiTableCell-stickyHeader、MuiTableCell-paddingCheckbox、MuiTablePagination-actions等)由tableClasses.ts/tableCellClasses.ts/tablePaginationClasses.ts生成,可用于精准 CSS 选择。
九、小结
| 场景 | 推荐方案 |
|---|---|
| 简单静态数据展示 | TableContainer + Table + 头/体结构,aria-label 或 <caption> 提供名称 |
| 密集信息 | Table size="small"(上下文自动下发至所有单元格) |
| 排序/多选/分页 | TableSortLabel(aria-sort 自动)+ TablePagination(放表格外防横向滚动,或放入 TableFooter) |
| 服务端分页 | count={-1} + rowsPerPageOptions 含 { value: -1, label: 'All' } |
| 长表格 | stickyHeader + 限高滚动容器 |
| 复杂数据表(编辑、树、分组) | 迁移到 @mui/x-data-grid 的 DataGrid |
| 客户端超大数组 | react-virtuoso 虚拟滚动示例 |
所有示例源码均可在 docs/data/material/components/table/ 目录按 BasicTable.tsx、DenseTable.tsx、EnhancedTable.tsx、StickyHeadTable.tsx、ColumnGroupingTable.tsx、CollapsibleTable.tsx、SpanningTable.tsx、ReactVirtualizedTable.tsx、CustomPaginationActionsTable.tsx、CustomizedTables.tsx、AccessibleTable.tsx 查看,组件实现集中在 packages/mui-material/src/Table/、TableCell/、TablePagination/ 三个目录,便于按本文结论对照阅读。
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