Material UI List 组件完全解析:从基础列表到虚拟化长列表的实现与定制
本篇基于 Material UI 仓库中 List 组件的官方文档,系统讲解 React List 组件家族的构成与用法,覆盖基础列表、嵌套列表、交互控件(Checkbox/Switch)、粘性子标题、alignItems 对齐、inset/disableGutters 布局控制、虚拟化长列表等全部核心场景,并结合 @mui/material 源码剖析 dense 上下文传播、secondaryAction 定位、选中态主题色计算等底层实现,帮助你既会用组件,也看得懂组件背后的样式机制。
List 组件家族:一整套组合式组件
按照 Material Design 规范,列表是"连续的、垂直的文本或图像索引",由包含主要操作与次要操作(以图标和文本呈现)的条目组成。Material UI 并没有提供一个"大而全"的 List 组件,而是实现为一组可自由组合的关联组件:
| 组件 | 职责 | 默认渲染元素 |
|---|---|---|
List |
列表项的容器 | <ul> |
ListItem |
通用列表项 | <li> |
ListItemButton |
列表项内的可点击/可操作元素(基于 ButtonBase) |
<div> |
ListItemIcon |
列表项内的图标包装 | span |
ListItemAvatar |
列表项内的头像包装 | span |
ListItemText |
列表项内文本内容容器,支持 primary/secondary 两行 |
span |
Divider |
列表项之间的分隔线 | hr |
ListSubheader |
嵌套列表/分组的标签 | li/div |
基础用法只需引入核心组件:
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
基础列表:BasicList
仓库中的示例 BasicList.tsx 演示了最典型的"邮箱文件夹"结构——注意外层用 <nav> 包裹并添加 aria-label,ListItem 使用 disablePadding 把内边距交给 ListItemButton 控制:
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Divider from '@mui/material/Divider';
import InboxIcon from '@mui/icons-material/Inbox';
import DraftsIcon from '@mui/icons-material/Drafts';
export default function BasicList() {
return (
<Box sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
<nav aria-label="main mailbox folders">
<List>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Inbox" />
</ListItemButton>
</ListItem>
{/* ... 其余列表项 */}
</List>
</nav>
<Divider />
<nav aria-label="secondary mailbox folders">
<List>
<ListItem disablePadding>
<ListItemButton component="a" href="#simple-list">
<ListItemText primary="Spam" />
</ListItemButton>
</ListItem>
</List>
</nav>
</Box>
);
}
其中渲染链接的方式值得单独强调:给 ListItemButton 传入 component="a" 与 href 即可将其变为锚点。由于 ListItemButton 底层是 ButtonBase,component 支持传入任意 HTML 标签或组件。若要与路由库集成(React Router 的 Link、Next.js 的 Link 等),仓库的路由集成文档提供了 ListRouter.js 示例,思路同样是 component={RouterLink}。
List 容器源码解析:dense 是如何传递给子项的
阅读 List.js 可以发现几个关键实现细节:
1. 默认渲染 <ul>,并清理默认样式。 根节点由 styled('ul') 生成,基础样式为:
listStyle: 'none',
margin: 0,
padding: 0,
position: 'relative',
component prop 默认为 'ul',可覆盖为 nav、div 等;未禁用 padding 时,容器上下各留 8px。
2. dense 通过 React Context 向后代传播。 源码中:
const context = React.useMemo(() => ({ dense }), [dense]);
// ...
<ListContext.Provider value={context}>
ListContext 使得 List dense 无需在每个 ListItem 上重复声明。在 ListItem.js 中可以看到消费逻辑:
const context = React.useContext(ListContext);
const childContext = React.useMemo(
() => ({
dense: dense || context.dense || false,
alignItems,
disableGutters,
}),
[alignItems, context.dense, dense, disableGutters],
);
也就是说子项 dense 的取值是"自身声明 || 父级上下文",并且会继续向下层 Provider 传递。ListItem 的默认垂直 padding 为 8px,dense 时缩减为 4px(见 ListItem.js 中 ListItemRoot 的 variants 定义)。
3. 提供语义化的 CSS 类名。 listClasses.ts 定义了 MuiList-root、MuiList-padding、MuiList-dense、MuiList-subheader 四个类名,配合 useUtilityClasses 按 disablePadding、dense、subheader 状态动态组合,便于外部通过 classes prop 或 CSS 选择器做精确覆盖。
4. subheader prop 的特殊处理。 当传入 subheader 时,容器会移除顶部 padding 并设置 isolation: 'isolate'(源码注释说明这是为了防止与 iOS 覆盖式滚动条重叠),子标题由此获得贴顶的视觉位置。
嵌套列表:ListSubheader + Collapse
嵌套列表(如"收件箱 → 星标"两级结构)的完整示例见 NestedList.tsx,其技术要点:
List的component="nav"并通过aria-labelledby关联子标题 id,保证可访问性;- 子标题通过
List的subheaderprop 注入(而非作为普通 children),对应上文源码中subheader的专门处理逻辑; - 展开/收起由
Collapse组件的inprop 驱动,timeout="auto"配合unmountOnExit让动画时长与高度自适应; - 子级
List设置component="div" disablePadding,子项用sx={{ pl: 4 }}(4 × spacing = 32px)产生缩进。
<List
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
Nested List Items
</ListSubheader>
}
>
{/* ... 一级列表项 */}
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
<ListItemButton sx={{ pl: 4 }}>
<ListItemIcon>
<StarBorder />
</ListItemIcon>
<ListItemText primary="Starred" />
</ListItemButton>
</List>
</Collapse>
</List>
列表控件:Checkbox 与 Switch 的主/次操作分工
Material Design 对列表内控件的位置有明确约定,Material UI 提供了对应示例:
- Checkbox 作为主要操作(
CheckboxList):复选框既是该列表项的主要操作,也是状态指示器,通常放在ListItemIcon的位置; - Checkbox 作为次要操作(
CheckboxListSecondary):复选框是独立的、与主操作(如整行点击)分离的目标; - Switch 作为次要操作(
SwitchListSecondary):开关放在列表项右端。
次要操作的实现依托 ListItem 的 secondaryAction prop。源码中这一点值得注意:ListItemRoot 的 variants 里,只要检测到 secondaryAction 存在,就为该行(以及内部的 ListItemButton)预留 paddingRight: 48,因为 ListItemSecondaryAction 是绝对定位在行右端的,必须留出碰撞空间:
{
props: ({ ownerState }) => !ownerState.disablePadding && !!ownerState.secondaryAction,
style: {
// Add some space to avoid collision as `ListItemSecondaryAction`
// is absolutely positioned.
paddingRight: 48,
},
},
多行文本对齐:alignItems="flex-start"
ListItem 默认 alignItems 为 'center'(垂直居中)。当列表项显示三行及以上内容时,头像/图标会跟着下沉,不符合 Material Design 规范。此时应设置 alignItems="flex-start" 让头像对齐顶部(示例见 AlignItemsList.tsx):
<ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar src="..." />
</ListItemAvatar>
<ListItemText primary="First" secondary="Second" />
</ListItem>
从源码结构看,这个 prop 同时作用于两层:ListItem 自身通过 variants 设置 align-items: flex-start,并通过 childContext 把 alignItems 传给 ListItemButton(其 useUtilityClasses 中会生成 MuiListItemButton-alignItemsFlexStart 类名),保证图标容器与文本容器一并对齐,而不是只对齐了外层 flex 容器。
粘性子标题:纯 CSS sticky 实现
滚动时子标题保持固定在视口顶部、直到被下一个子标题顶出屏幕——这一效果完全依赖 CSS sticky 定位,示例见 PinnedSubheaderList.tsx:
<List
sx={{
width: '100%',
maxWidth: 360,
bgcolor: 'background.paper',
position: 'relative',
overflow: 'auto',
maxHeight: 300,
'& ul': { padding: 0 },
}}
subheader={<li />}
>
{[0, 1, 2, 3, 4].map((sectionId) => (
<li key={`section-${sectionId}`}>
<ul>
<ListSubheader>{`I'm sticky ${sectionId}`}</ListSubheader>
{[0, 1, 2].map((item) => (
<ListItem key={`item-${sectionId}-${item}`}>
<ListItemText primary={`Item ${item}`} />
</ListItem>
))}
</ul>
</li>
))}
</List>
几个易被忽略的细节:
overflow必须在容器上而不是body上——sticky 元素相对最近的滚动祖先定位,因此示例手动给List设置了position: 'relative'、overflow: 'auto'、maxHeight: 300来制造滚动区域;subheader={<li />}:List的subheaderprop 会渲染在 children 之前,这里放一个空<li>是为了让内部按"每节一个<li><ul>"分组的 HTML 结构保持合法嵌套;ListSubheader自身使用position: sticky,滚动时逐节"接力"吸附。
inset 与 disableGutters:两种布局微调
Inset 列表项:inset prop 让没有前置图标或头像的列表项,与有图标的列表项在文本起始位置上正确对齐(示例 InsetList.tsx)。它通常用在同一列表内"有图标项 + 无图标项 + 展开的无图标子项"混排的场景(如"音乐列表"模式)。
无槽列表(Gutterless):当列表被渲染在自身定义了左右内边距的容器(如 Drawer、Paper)内部时,ListItem 默认的 16px 左右 padding(gutters)会造成双层缩进。此时对 ListItem(或 ListItemButton)设置 disableGutters 即可移除左右 padding。从源码看,disableGutters 同样经由 ListContext 参与 ListItem 的样式变体判断,与 disablePadding(移除上下 padding)是正交的两个开关:
| prop | 影响 | 源码默认 padding |
|---|---|---|
disablePadding(List/ListItem) |
移除上下垂直 padding | List/ListItem 各 8px,dense 时 4px |
disableGutters(ListItem/ListItemButton) |
移除左右水平 padding | 16px |
选中态:selected 背后的主题色计算
选中列表项的示例见 SelectedListItem.tsx,核心就是 ListItemButton 的 selected prop。ListItemButton.js 中可以看到选中态并非写死的颜色,而是由主题动态合成:
[`&.${listItemButtonClasses.selected}`]: {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
(theme.vars || theme).palette.action.selectedOpacity,
),
}
即"主题主色 × action.selectedOpacity 透明度"。悬停选中项时,颜色为 selectedOpacity + hoverOpacity 的叠加;在触摸设备(@media (hover: none))上则回退为纯选中色。此外源码还处理了可访问性细节:当启用 theme.focusVisible 时,聚焦环采用 inset 形式(源码注释解释:滚动容器会裁掉外扩的 focus ring),禁用态则应用 action.disabledOpacity 透明度。
虚拟化列表:List 遇上 react-window
长列表(成百上千条数据)直接渲染会产生明显的性能问题。文档推荐配合 react-window 使用,示例见 VirtualizedList.tsx——渲染 200 行,实际只会挂载可视区域内的 DOM:
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import { List, RowComponentProps } from 'react-window';
function renderRow(props: RowComponentProps) {
const { index, style } = props;
return (
<ListItem style={style} key={index} component="div" disablePadding>
<ListItemButton>
<ListItemText primary={`Item ${index + 1}`} />
</ListItemButton>
</ListItem>
);
}
export default function VirtualizedList() {
return (
<Box sx={{ width: '100%', height: 400, maxWidth: 360, bgcolor: 'background.paper' }}>
<List
rowHeight={46}
rowCount={200}
style={{ height: 400, width: 360 }}
rowProps={{}}
overscanCount={5}
rowComponent={renderRow}
/>
</Box>
);
}
关键点:这里的 List 来自 react-window 而非 @mui/material,rowComponent 中返回的是 Material UI 的 ListItem(component="div" 因为虚拟化行不是 <li>)。react-window 的 List 要求每行高度固定,所以传入了 rowHeight={46};overscanCount={5} 表示在可视区域上下各多渲染 5 行以消除滚动白边。文档同时建议:react-window 无法满足需求时可考虑 react-virtuoso 等替代方案。
样式定制:类名、overrides 与 sx
List 家族支持三套递进的定制手段:
sxprop:组件级的快速样式注入(如示例中的bgcolor: 'background.paper');classesprop + 语义类名:如MuiList-root、MuiList-dense、MuiListItem-gutters、MuiListItemButton-selected,各组件的类名清单分别在 listClasses.ts、packages/mui-material/src/ListItem/listItemClasses.ts、packages/mui-material/src/ListItemButton/listItemButtonClasses.ts中集中定义;- 主题级 overrides:通过
theme.components.MuiList.styleOverrides等做全局覆盖,每个组件源码中的overridesResolver决定了主题样式按状态(dense、divider、selected…)合并到哪个 slot。
仓库提供了完整的定制化示例 CustomizedList.tsx,主题覆盖机制的详细说明见定制文档。
Props 速查表
List(源码):
| prop | 默认值 | 说明 |
|---|---|---|
component |
'ul' |
根节点渲染的 HTML 元素或组件 |
dense |
false |
紧凑垂直 padding;通过 Context 传递给所有后代列表项 |
disablePadding |
false |
移除容器上下 8px padding |
subheader |
— | 子标题内容,通常是 ListSubheader,渲染在 children 之前 |
ListItem(源码):
| prop | 默认值 | 说明 |
|---|---|---|
component |
'li' |
根节点渲染元素 |
alignItems |
'center' |
可选 'flex-start',多行时让头像顶部对齐 |
dense |
false |
优先继承父级 List 的 dense 上下文 |
disableGutters |
false |
移除左右 16px padding |
disablePadding |
false |
移除上下 8px padding |
divider |
false |
底部添加 1px 分隔线(取 theme.palette.divider 颜色) |
secondaryAction |
— | 行尾绝对定位的次要操作(图标按钮、Switch 等),自动预留 48px 右侧空间 |
slots / slotProps |
{} |
可替换 root、secondaryAction 两个 slot 的组件及其 props |
ListItemButton(源码):基于 ButtonBase,除上述布局 props 外,还支持 selected(选中底色 = 主色 × selectedOpacity)、disabled(应用 action.disabledOpacity)、component(传 'a' 变锚点、传路由 Link 组件做导航)等 ButtonBase 能力。
小结
Material UI 的 List 是一个典型的"组合式组件"设计:List 只负责容器、dense 上下文传播与类名管理,ListItem 负责行布局与 gutter/padding/divider/对齐等状态变体,ListItemButton 复用 ButtonBase 提供完整的可点击、选中、禁用与焦点行为。理解 List.js 的 Context 机制、ListItem.js 中基于 ownerState 的 variants 样式表,以及 ListItemButton.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 StartedRust0626
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