首页
/ Material UI List 组件完全解析:从基础列表到虚拟化长列表的实现与定制

Material UI List 组件完全解析:从基础列表到虚拟化长列表的实现与定制

2026-09-06 09:57:24作者:柯茵沙

本篇基于 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-labelListItem 使用 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 底层是 ButtonBasecomponent 支持传入任意 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',可覆盖为 navdiv 等;未禁用 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.jsListItemRoot 的 variants 定义)。

3. 提供语义化的 CSS 类名。 listClasses.ts 定义了 MuiList-rootMuiList-paddingMuiList-denseMuiList-subheader 四个类名,配合 useUtilityClassesdisablePaddingdensesubheader 状态动态组合,便于外部通过 classes prop 或 CSS 选择器做精确覆盖。

4. subheader prop 的特殊处理。 当传入 subheader 时,容器会移除顶部 padding 并设置 isolation: 'isolate'(源码注释说明这是为了防止与 iOS 覆盖式滚动条重叠),子标题由此获得贴顶的视觉位置。

嵌套列表:ListSubheader + Collapse

嵌套列表(如"收件箱 → 星标"两级结构)的完整示例见 NestedList.tsx,其技术要点:

  • Listcomponent="nav" 并通过 aria-labelledby 关联子标题 id,保证可访问性;
  • 子标题通过 Listsubheader prop 注入(而非作为普通 children),对应上文源码中 subheader 的专门处理逻辑;
  • 展开/收起由 Collapse 组件的 in prop 驱动,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):开关放在列表项右端。

次要操作的实现依托 ListItemsecondaryAction 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,并通过 childContextalignItems 传给 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>

几个易被忽略的细节:

  1. overflow 必须在容器上而不是 body——sticky 元素相对最近的滚动祖先定位,因此示例手动给 List 设置了 position: 'relative'overflow: 'auto'maxHeight: 300 来制造滚动区域;
  2. subheader={<li />}Listsubheader prop 会渲染在 children 之前,这里放一个空 <li> 是为了让内部按"每节一个 <li><ul>"分组的 HTML 结构保持合法嵌套;
  3. ListSubheader 自身使用 position: sticky,滚动时逐节"接力"吸附。

insetdisableGutters:两种布局微调

Inset 列表项inset prop 让没有前置图标或头像的列表项,与有图标的列表项在文本起始位置上正确对齐(示例 InsetList.tsx)。它通常用在同一列表内"有图标项 + 无图标项 + 展开的无图标子项"混排的场景(如"音乐列表"模式)。

无槽列表(Gutterless):当列表被渲染在自身定义了左右内边距的容器(如 DrawerPaper)内部时,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,核心就是 ListItemButtonselected 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/materialrowComponent 中返回的是 Material UI 的 ListItemcomponent="div" 因为虚拟化行不是 <li>)。react-window 的 List 要求每行高度固定,所以传入了 rowHeight={46}overscanCount={5} 表示在可视区域上下各多渲染 5 行以消除滚动白边。文档同时建议:react-window 无法满足需求时可考虑 react-virtuoso 等替代方案。

样式定制:类名、overrides 与 sx

List 家族支持三套递进的定制手段:

  1. sx prop:组件级的快速样式注入(如示例中的 bgcolor: 'background.paper');
  2. classes prop + 语义类名:如 MuiList-rootMuiList-denseMuiListItem-guttersMuiListItemButton-selected,各组件的类名清单分别在 listClasses.tspackages/mui-material/src/ListItem/listItemClasses.tspackages/mui-material/src/ListItemButton/listItemButtonClasses.ts 中集中定义;
  3. 主题级 overrides:通过 theme.components.MuiList.styleOverrides 等做全局覆盖,每个组件源码中的 overridesResolver 决定了主题样式按状态(densedividerselected…)合并到哪个 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 {} 可替换 rootsecondaryAction 两个 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 的主题色合成逻辑,就掌握了从"照抄示例"到"按主题规范深度定制"所需的全部依据。

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