Material UI Accordion 组件深度指南:展开/折叠面板的 API、源码实现与最佳实践
Material UI 的 Accordion(手风琴)组件用于在页面上以可展开/折叠的方式展示相关联的内容区块。本篇基于当前仓库的官方文档 docs/data/material/components/accordion/accordion.md 与 packages/mui-material/src 下的真实源码编写,覆盖 Accordion 系列的完整 API、受控/非受控用法、转场定制、无障碍(WAI-ARIA)要求、DOM 结构解剖,以及基于源码验证的性能优化策略,帮助你在 React 项目中正确且高效地构建手风琴交互。
说明:Accordion 已不再出现在最新版 Material Design 官方指南中,但仓库文档明确承诺 Material UI 会继续维护该组件(见 accordion.md 中的提示),可以放心用于生产项目。
组件家族:四个互补组件的分工
Accordion 不是一个单体组件,而是由四个职责清晰的组件组合而成(文档 "Introduction" 一节):
- Accordion:外层包装器,负责分组相关组件、管理展开状态;
- AccordionSummary:头部包装器,点击时展开或折叠内容;
- AccordionDetails:内容区包装器,承载折叠区内具体 UI;
- AccordionActions:可选包装器,用于把一组按钮分组放在面板底部。
基础引入方式:
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
从源码结构看,Accordion 组件通过解构 children 的第一个元素将其识别为 summary(见 Accordion.js 中 const [summary, ...children] = React.Children.toArray(childrenProp);),并通过 PropTypes 校验强制要求"第一个子元素必须是合法的 React 元素,且不接受 Fragment"(见 Accordion.js)。这意味着 <Accordion> 内第一个子节点必须是 <AccordionSummary>,后续子节点渲染进内容区。
AccordionSummary 内部基于 ButtonBase 实现,点击时会先调用 Context 中的 toggle(event) 翻转展开状态,再触发用户传入的 onClick(见 AccordionSummary.js);它还会自动设置 aria-expanded 并禁用涟漪效果(focusRipple: false, disableRipple: true,见 AccordionSummary.js),因为手风琴头部本身是一个"看起来像标题"的按钮,不需要按钮的默认视觉反馈。
AccordionDetails 的实现则非常薄——只是一个带 padding: theme.spacing(1, 2, 2) 的 div(见 AccordionDetails.js),所有间距与视觉样式都交给用户通过 sx 或 theme 定制。
完整的最简用法可参考仓库中的演示文件 AccordionUsage.js。
基础用法
展开指示图标(expandIcon)
通过 AccordionSummary 的 expandIcon prop 更换展开指示图标。图标"翻转 180°"的动画由组件自动处理:从源码可见 expandIconWrapper 插槽默认 transform: 'rotate(0deg)',展开态追加 .expanded 类后变为 rotate(180deg),并带有 theme.transitions.duration.shortest 的过渡(见 AccordionSummary.js):
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="p1c" id="p1h">
Accordion 1
</AccordionSummary>
默认展开(defaultExpanded)
使用 Accordion 的 defaultExpanded prop(默认 false)让面板初始处于展开状态:
<Accordion defaultExpanded>...</Accordion>
单元测试 Accordion.test.js 验证了该行为:渲染 <Accordion defaultExpanded> 后,根元素会带有 MuiAccordion-expanded 类名。示例见 AccordionExpandDefault.js。
更换默认转场(slots.transition)
使用 slots.transition 和 slotProps.transition 可以替换默认的 Collapse 转场组件。源码中 transition 插槽的默认元素正是 Collapse,且 Accordion 传入 in={expanded} timeout="auto"(见 Accordion.js):
<Accordion
slots={{ transition: Fade }}
slotProps={{ transition: { timeout: 900 } }}
>
...
</Accordion>
timeout="auto" 意味着转场时长会从子元素的 CSS 过渡中自动测量;如果自定义转场组件不支持 auto,可显式传入 timeout。替换转场的合法前提是自定义组件实现 Material UI 的 Transition 接口——仓库测试中甚至用一个极简的 NoTransition 组件验证了该插槽的可替换性(见 Accordion.test.js),describeConformance 中还专门把 heading 插槽替换为 h4 验证了标题层级可换(见 Accordion.test.js)。示例见 AccordionTransition.js。
禁用项(disabled)
在 Accordion 上设置 disabled prop 即可同时禁用交互与焦点:
<Accordion disabled>
<AccordionSummary expandIcon={<ExpandMoreIcon />} id="d1h" aria-controls="d1c">
Disabled Accordion
</AccordionSummary>
</Accordion>
源码实现链路上,disabled 状态经由 AccordionContext 下发给 AccordionSummary(见 Accordion.js),Summary 再以 disabled 传给内部 ButtonBase 并应用 palette.action.disabledOpacity 的半透明样式(见 AccordionSummary.js)。示例见 DisabledAccordion.js。
受控 Accordion(Controlled)
Accordion 既可以是受控(由父组件通过 props 管理状态),也可以是非受控(组件自身本地状态管理):
- 受控:传入
expanded+onChange,组件只反映外部状态,测试 Accordion.test.js 验证了setProps({ expanded: false })会移除expanded类名; - 非受控:只传
defaultExpanded或不传任何状态 prop,状态保存在组件内部。
源码中这一双模式由 useControlled 钩子实现(见 Accordion.js),内部 handleChange 回调在每次切换后以 (event, !expanded) 形式通知 onChange。
"同一时间只允许一个面板展开"的经典实现(文档 "Only one expanded at a time" 一节的完整模式,摘自 ControlledAccordions.js):
import * as React from 'react';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
export default function ControlledAccordions() {
const [expanded, setExpanded] = React.useState(false);
const handleChange = (panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
return (
<div>
<Accordion expanded={expanded === 'panel1'} onChange={handleChange('panel1')}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography component="span" sx={{ width: '33%', flexShrink: 0 }}>
General settings
</Typography>
<Typography component="span" sx={{ color: 'text.secondary' }}>
I am an accordion
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat.
Aliquam eget maximus est, id dignissim quam.
</Typography>
</AccordionDetails>
</Accordion>
{/* 其余 panel2 / panel3 / panel4 结构相同,仅 id 与文案不同 */}
</div>
);
}
该模式的要点:expanded 保存当前展开面板的标识(如 'panel1'),点击已展开面板时 isExpanded 为 false,状态回落到 false——即允许"全部收起"。
核心 Props 参考(以仓库类型定义为准)
以下为 Accordion 的全部自有 props(摘自 Accordion.d.ts),此外还继承了 Paper 的全部 props(elevation、square、variant 等)与 sx:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
children |
ReactNode(必需) |
— | 第一个子元素必须是 AccordionSummary,其余渲染进内容区 |
defaultExpanded |
boolean |
false |
非受控模式下初始是否展开 |
expanded |
boolean |
— | 受控展开状态;设置后即进入受控模式 |
onChange |
(event, expanded: boolean) => void |
— | 展开/折叠状态变化回调;注意 event 是通用合成事件而非 change 事件 |
disabled |
boolean |
false |
禁用交互与焦点,头部按钮变半透明 |
disableGutters |
boolean |
false |
移除展开时面板间的 16px 0 外边距,避免展开撑高页面(对应 CSS variant &.expanded { margin: '16px 0' },见 Accordion.js) |
classes / className |
object / string |
— | 工具类名覆盖与根类名 |
sx |
SxProps<Theme> |
— | 样式系统 prop |
slots |
{ root?, heading?, region?, transition? } |
{} |
替换内部组件;默认分别为 Paper、'h3'、'div'、Collapse |
slotProps |
各插槽的 props | {} |
向各内部插槽透传 props;heading 插槽即通过它改标题层级 |
AccordionSummary 自有 props 包括:children、expandIcon(展开指示图标)、focusVisibleClassName(键盘焦点高亮类,:focus-visible 的 polyfill 方案)、onClick、classes、sx 及 slots/slotProps(root、content、expandIconWrapper 三个插槽)。id 与 aria-controls 会透传到内部按钮上(无障碍要求,见下文)。
定制:styled 覆盖与标题层级
视觉定制(styled 包装)
文档示例 CustomizedAccordions.js 展示了用 styled 包装三个子组件实现"无投影、直角、分隔线列表"风格的完整写法:
import { styled } from '@mui/material/styles';
import MuiAccordion from '@mui/material/Accordion';
import MuiAccordionSummary, {
accordionSummaryClasses,
} from '@mui/material/AccordionSummary';
import MuiAccordionDetails from '@mui/material/AccordionDetails';
const Accordion = styled((props) => (
<MuiAccordion disableGutters elevation={0} square {...props} />
))(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
'&:not(:last-child)': { borderBottom: 0 },
'&::before': { display: 'none' }, // 隐藏默认的顶部分隔线伪元素
}));
const AccordionSummary = styled((props) => (
<MuiAccordionSummary
expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: '0.9rem' }} />}
{...props}
/>
))(({ theme }) => ({
backgroundColor: 'rgba(0, 0, 0, .03)',
flexDirection: 'row-reverse',
[`& .${accordionSummaryClasses.expandIconWrapper}.${accordionSummaryClasses.expanded}`]: {
transform: 'rotate(90deg)', // 覆盖默认 180 度翻转
},
[`& .${accordionSummaryClasses.content}`]: {
marginLeft: theme.spacing(1),
},
...theme.applyStyles('dark', { backgroundColor: 'rgba(255, 255, 255, .05)' }),
}));
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
padding: theme.spacing(2),
borderTop: '1px solid rgba(0, 0, 0, .125)',
}));
配合上文的受控 useState 逻辑即可得到文档演示中的"只展开一个 + 视觉定制"效果。其中 accordionSummaryClasses 命名导出(content、expandIconWrapper、expanded 等)来自 accordionSummaryClasses.ts,Accordion 自身同样导出 accordionClasses(root、heading、rounded、expanded、disabled、gutters、region,见 accordionClasses.ts),可用于 sx 选择器或 CSS 深度定制。
从源码看,默认外观的许多细节都来自伪元素:&::before 是一条 1px 的顶部分隔线,展开时 opacity: 0 淡出,首个面板(:first-of-type)隐藏分隔线(见 Accordion.js);圆角则由 rounded variant 按 :first-of-type/:last-of-type 只保留两端圆角、中间保持直角(见 Accordion.js)。因此用 &::before { display: 'none' } 或 square 可以显著简化定制。
修改标题层级(slotProps.heading.component)
Accordion 默认用 h3 元素渲染标题(源码中 AccordionHeading = styled('h3', ...),见 Accordion.js)。为保持文档标题层级正确,可用 slotProps.heading.component 更换:
<Accordion slotProps={{ heading: { component: 'h4' } }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1-content"
id="panel1-header"
>
Accordion
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</AccordionDetails>
</Accordion>
也可以直接换组件:slots={{ heading: 'h4' }},仓库测试已验证 heading 插槽替换为 h4 时类名仍正确应用(见 Accordion.test.js)。
性能:内容默认挂载与 unmountOnExit
文档 "Performance" 一节指出的关键默认行为是:Accordion 内容即使未展开也会被挂载(内容始终存在于 DOM 中,仅通过转场控制可见性)。这一默认取舍是为服务端渲染(SSR)与 SEO 考虑的——折叠内容对爬虫和首屏 HTML 依然可见。
当折叠区内部嵌入了较大的组件树、或页面上有大量 Accordion 时,可通过 slotProps.transition.unmountOnExit 改为"折叠即卸载":
<Accordion slotProps={{ transition: { unmountOnExit: true } }} />
unmountOnExit 是 Material UI 转场组件(Collapse 等)的标准 prop:转场结束后立即从 DOM 移除子树,减少内存占用与后续渲染成本。从源码看,Accordion 把 slotProps.transition 原样转发给默认 Collapse(见 Accordion.js),因此所有 Collapse 支持的 prop 均可透传。
无障碍(WAI-ARIA)
遵循 WAI-ARIA 的 Accordion 模式(文档 frontmatter 中的 waiAria 链接指向该规范),需要在 AccordionSummary 上设置 id 与 aria-controls:
<Accordion>
<AccordionSummary id="panel-header" aria-controls="panel-content">
Header
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</AccordionDetails>
</Accordion>
剩下的关联是组件自动完成的——源码中 region 插槽直接从 summary 的 props 推导:aria-labelledby 取 summary 的 id,自身 id 取 summary 的 aria-controls,并固定 role="region"(见 Accordion.js):
// Accordion.js 内部(简化)
const [AccordionRegionSlot, accordionRegionProps] = useSlot('region', {
elementType: AccordionRegion,
...
additionalProps: {
'aria-labelledby': summary.props.id,
id: summary.props['aria-controls'],
role: 'region',
},
});
因此在 Summary 上手动写好 id + aria-controls 后,内容区的 role="region"、id、aria-labelledby 三者自动配对,无需重复声明。React 18+ 下可用 React.useId() 生成唯一 id,避免多个面板 id 冲突(DisabledAccordion.js 演示了这种写法)。
DOM 结构解剖(Anatomy)
文档给出的静态结构如下,可用作 CSS 选择器与测试断言的稳定依据:
<div class="MuiAccordion-root">
<h3 class="MuiAccordion-heading">
<button class="MuiButtonBase-root MuiAccordionSummary-root" aria-expanded="">
<!-- Accordion summary goes here -->
</button>
</h3>
<div class="MuiAccordion-region" role="region">
<div class="MuiAccordionDetails-root">
<!-- Accordion content goes here -->
</div>
</div>
</div>
与源码对照确认的要点:
- 根元素是
Paper(而非普通div),所以默认带投影(elevation),可用elevation={0}去除,也可用slots={{ root: ... }}整体替换; - 头部按钮的
aria-expanded随展开状态同步(测试断言aria-expanded='false',见 Accordion.test.js); - 内容实际包在 transition 插槽(默认 Collapse) 与 region 之间:渲染树为
Root → heading(summary) + TransitionSlot → Region → children(见 Accordion.js),这也解释了slotProps.transition的作用位置; - 状态类名(
MuiAccordion-expanded、MuiAccordion-disabled、MuiAccordion-gutters等)均由composeClasses依据ownerState组合到根与子元素上(见 Accordion.js)。
相关资源
- 组件源码:packages/mui-material/src/Accordion/、AccordionSummary/、AccordionDetails/、AccordionActions/
- 单元测试:Accordion.test.js(覆盖受控/非受控切换、
onChange参数、插槽替换、无障碍属性) - 演示源码:docs/data/material/components/accordion/ 下的
ControlledAccordions.js、CustomizedAccordions.js、DisabledAccordion.js、AccordionTransition.js等 - 官方文档:accordion.md
小结
- Accordion 家族四组件各司其职;
children首元素必须是AccordionSummary,这是源码 PropTypes 的硬校验; - 受控与"单开互斥"模式通过
expanded+onChange+ 父级useState实现,useControlled保证两种模式可无缝切换; - 四个内部插槽
root / heading / transition / region分别默认Paper / h3 / Collapse / div,是定制视觉与性能的主要入口; - 无障碍只需在 Summary 上写
id与aria-controls,region 的 ARIA 关联由组件自动推导; - 需要性能优化时,用
slotProps.transition.unmountOnExit把默认"始终挂载"改为"折叠即卸载",代价是 SSR 首屏 HTML 中不再包含折叠内容。
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