首页
/ Material UI Stack 组件实战:一维 Flex 布局中的 spacing、direction、divider 与 useFlexGap 深度解析

Material UI Stack 组件实战:一维 Flex 布局中的 spacing、direction、divider 与 useFlexGap 深度解析

2026-09-06 21:34:06作者:郁楠烈Hubert

本篇指南聚焦 Material UI 的 Stack 组件——一个用于将子元素沿垂直或水平方向排列的通用布局容器。我们将完整覆盖 Stack 的核心属性(spacingdirectiondivideruseFlexGapsx)及其响应式用法,并结合开源仓库中 @mui/material@mui/system 的源码实现,说明 spacing 的 margin 实现机制、断点解析原理与两类已知限制(子元素 margin 被覆盖、white-space: nowrap 定位冲突)的成因与解法。读完后你可以独立使用 Stack 构建一维布局,并能从源码层面理解其行为边界。

Stack 的定位:一维布局容器

Stack 负责管理其直接子元素在垂直或水平轴上的排列,并支持在子元素之间插入间距(spacing)或分隔元素(divider)。它适合一维布局场景;当需要同时处理垂直和水平两个维度的网格排列时,应改用 Grid 组件。

文档页面(stack.md)明确给出了这一定位:

Stack is ideal for one-dimensional layouts, while Grid is preferable when you need both vertical and horizontal arrangement.

Stack 的默认排列方向为 column,即子元素纵向堆叠。

基本用法与 spacing 间距控制

基础导入方式:

import Stack from '@mui/material/Stack';

Stack 是一个通用容器,包裹需要排列的元素。使用 spacing 属性控制子元素之间的间距。spacing 可以是任意数字(包括小数)或字符串,该属性会通过主题的 theme.spacing() 辅助函数转换为 CSS 值:

import Stack from '@mui/material/Stack';

export default function BasicStack() {
  return (
    <Stack spacing={2}>
      <Item>Item 1</Item>
      <Item>Item 2</Item>
      <Item>Item 3</Item>
    </Stack>
  );
}

上述示例对应仓库中的演示文件 BasicStack.tsx,其中 Item 是一个基于 Paper 的 styled 组件,spacing={2} 表示子元素之间的间距为 2 个 spacing 单位(默认即 2 * theme.spacing() = 8px)。

spacing 的类型签名为 ResponsiveStyleValue<number | string>(见 Stack.d.ts),因此它还支持响应式对象形式(后文详述)。

direction:控制排列方向

默认情况下 Stack 将子元素纵向排列为 column。使用 direction 属性可将子元素横向排列为 row:

<Stack direction="row" spacing={2}>
  <Item>Item 1</Item>
  <Item>Item 2</Item>
  <Item>Item 3</Item>
</Stack>

对应演示文件 DirectionStack.tsxdirection 接受 CSS flex-direction 的全部取值:'row''row-reverse''column''column-reverse'(类型定义见 Stack.d.ts)。从源码默认值看(createStack.tsx),direction 的默认值正是 'column'

divider:在子元素之间插入分隔元素

divider 属性用于在每对相邻子元素之间插入一个 React 元素,与 Divider 组件配合使用效果尤佳:

import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';

<Stack
  direction="row"
  divider={<Divider orientation="vertical" flexItem />}
  spacing={2}
>
  <Item>Item 1</Item>
  <Item>Item 2</Item>
  <Item>Item 3</Item>
</Stack>

对应演示文件 DividerStack.tsx

源码实现:分隔元素的插入逻辑位于 createStack.tsxjoinChildren 函数中。该函数通过 React.Children.toArray(children).filter(Boolean) 将子元素转为数组(并过滤掉 falsy 节点),然后 reduce 时在每个子元素之后(最后一个除外)推入一个 React.cloneElement(separator, { key: separator-${index} })——即为分隔元素克隆并附上 separator-0separator-1 这样的 key 以满足 React 列表渲染要求。这意味着:

  • divider 只作用于直接子元素,不会递归插入嵌套结构内部;
  • 每个分隔元素是独立克隆的,可以为每个分隔元素传递不同的 props(通过 children 的 key 区分场景)。

响应式值:按断点切换 direction 与 spacing

directionspacing 都支持响应式对象,按当前激活的断点切换取值:

<Stack
  direction={{ xs: 'column', sm: 'row' }}
  spacing={{ xs: 1, sm: 2, md: 4 }}
>
  <Item>Item 1</Item>
  <Item>Item 2</Item>
  <Item>Item 3</Item>
</Stack>

对应演示文件 ResponsiveStack.tsx:在 xs 断点下子元素纵向排列、间距为 1 个单位;sm 起切换为横向排列、间距 2;md 起间距扩大到 4。

源码机制:在 createStack.tsx 的 style 函数 中,direction 通过 resolveBreakpointValues + handleBreakpoints 解析为逐断点的 flexDirection 声明。一个值得注意的细节(L119-L128):当 direction 为响应式对象而某个断点缺省时,源码会用前一个断点的方向值回填(previousDirectionValue,最终回退到 'column'),保证 margin 间距模式下 getSideFromDirection 在每个断点上都能取到正确的边。

Flexbox gap:useFlexGap 属性

默认情况下,Stack 的间距是通过给子元素施加 margin 实现的。若改用 CSS flexbox 的 gap 来实现间距,将 useFlexGap 设为 true 即可:

<Stack spacing={{ xs: 1, sm: 2 }} direction="row" useFlexGap sx={{ flexWrap: 'wrap' }}>
  <Item>Item 1</Item>
  <Item>Item 2</Item>
  <Item>Long content</Item>
</Stack>

对应演示文件 FlexboxGapStack.tsx。官方提示:gap 实现消除了默认 margin 实现的已知限制(见下文「限制」一节),但 CSS flexbox gap 在部分浏览器中并非完全支持,建议启用前自行确认目标浏览器的支持情况。

源码中的两种分支createStack.tsx L130-L146):

const styleFromPropValue = (propValue, breakpoint) => {
  if (ownerState.useFlexGap) {
    return { gap: getValue(transformer, propValue) };
  }
  return {
    '& > :not(style):not(style)': { margin: 0 },
    '& > :not(style) ~ :not(style)': {
      [`margin${getSideFromDirection(/* 方向 */)}`]: getValue(transformer, propValue),
    },
  };
};
  • useFlexGaptrue 时,直接输出 gap
  • false(默认)时,使用嵌套选择器 & > :not(style) ~ :not(style) 给除第一个外的每个直接子元素设置 margin。边的选择由 getSideFromDirection 根据方向映射:row → margin-leftrow-reverse → margin-rightcolumn → margin-topcolumn-reverse → margin-bottom

通过主题全局启用 useFlexGap

如果希望所有 Stack 实例默认使用 flexbox gap,可以在主题中为 MuiStack 设置默认 props:

import { ThemeProvider, createTheme } from '@mui/material/styles';
import Stack from '@mui/material/Stack';

const theme = createTheme({
  components: {
    MuiStack: {
      defaultProps: {
        useFlexGap: true,
      },
    },
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Stack></Stack> {/* 默认使用 flexbox gap */}
    </ThemeProvider>
  );
}

使用 sx 属性快速定制

Stack 支持 sx 属性,可访问 MUI System 包暴露的全部样式函数与主题感知属性。例如应用居中对齐:

<Stack sx={{ alignItems: 'center' }} />

sx 的类型为 SxProps<Theme>Stack.d.ts),因此可以写静态对象、函数或响应式数组。

已知限制及解法

限制一:子元素的自定义 margin 会被覆盖

默认实现(margin 模式)下,子元素自身的 margin 会被 Stack 重置,因此不支持在子元素上自定义 margin。例如:

<Stack>
  <Button sx={{ marginTop: '30px' }}>...</Button>
</Stack>

上面的 marginTop: '30px' 会被忽略。

原因(源码层面):如前文 styleFromPropValue 所示,非 gap 模式会输出 '& > :not(style):not(style)': { margin: 0 } 来避免双重间距,源码注释也写得很直白:

The useFlexGap={false} implement relies on each child to give up control of the margin. We need to reset the margin to avoid double spacing.

(见 createStack.tsx L134-L139

解法:将 useFlexGap 设为 true,切换到 CSS flexbox gap 实现,子元素即可自由控制自身 margin。

限制二:white-space: nowrap 引起的定位冲突

flex 项的初始 min-widthauto,当子元素使用 white-space: nowrap;(如 TypographynoWrap)时会引发定位冲突——长文本会撑破容器。最小复现:

<Stack direction="row">
  <Typography noWrap></Typography>
</Stack>

要让该项留在容器内部,需要将 min-width 置为 0

<Stack direction="row" sx={{ minWidth: 0 }}>
  <Typography noWrap></Typography>
</Stack>

对应演示 ZeroWidthStack.tsx。该演示直观对比了两种写法:直接嵌套 <Typography noWrap>Stack(长文本溢出),以及把 Typography 包进一个 sx={{ minWidth: 0 }} 的内层 Stack 后文本被正常截断的方案。

组件实现结构:从 @mui/system 到 @mui/material

从源码结构看,Stack 的实际逻辑在 @mui/system 中,@mui/material 仅做薄封装。packages/mui-material/src/Stack/Stack.js 的完整实现只有几行:

import { createStack } from '@mui/system';
import styled from '../styles/styled';
import { useDefaultProps } from '../DefaultPropsProvider';

const Stack = createStack({
  createStyledComponent: styled('div', {
    name: 'MuiStack',
    slot: 'Root',
  }),
  useThemeProps: (inProps) => useDefaultProps({ props: inProps, name: 'MuiStack' }),
});
  • createStack 工厂(createStack.tsx L155-L239)接收三个可选配置:createStyledComponent(自定义 styled 函数)、useThemeProps(默认值注入方式)、componentName(默认为 'MuiStack');
  • Material UI 版本传入 Material 的 styled 实现并以 MuiStack 命名,从而使主题的 components.MuiStack 配置(如前文的 defaultProps)能够生效;
  • @mui/system 的独立版本见 packages/mui-system/src/Stack/Stack.tsx,它直接调用无参的 createStack(),使用 System 默认主题。

组件通过 React.forwardRef 暴露 ref,根节点默认为 div,并可通过 component 属性替换为其他 HTML 元素或组件。

Anatomy:DOM 结构

Stack 渲染为单个根 <div> 元素,class 为 MuiStack-root

<div class="MuiStack-root">
  <!-- Stack contents -->
</div>

工具类由 packages/mui-material/src/Stack/stackClasses.ts 生成(generateUtilityClasses('MuiStack', ['root'])),目前仅有 root 一个 slot。

API 摘要与默认值

综合文档与 Stack.d.ts 类型定义,Stack 的完整属性一览:

属性 类型 默认值 说明
children ReactNode 组件内容
component ElementType 'div' 根节点使用的 HTML 元素或组件
direction ResponsiveStyleValue<'row' | 'row-reverse' | 'column' | 'column-reverse'> 'column' 定义 flex-direction,支持按断点取值
spacing ResponsiveStyleValue<number | string> 0 直接子元素之间的间距,经 theme.spacing() 转换
divider ReactNode 插入到每对相邻子元素之间的元素
useFlexGap boolean false 使用 flexbox gap 代替给子元素施加 margin
sx SxProps<Theme> 系统属性,定义样式覆写与额外 CSS

行为补充(源码依据):

  • spacing 为响应式对象时,direction 缺省的断点会用前一断点的方向回填(createStack.tsx L119-L128);
  • 非 gap 模式下,所有直接子元素的 margin 会被置 0,再对除首个外的子元素施加方向相关的 margin(createStack.tsx L134-L145);
  • divider 只对直接子元素生效,分隔元素经 cloneElement 克隆并带 separator-${index} key(createStack.tsx L52-L64)。

相关源码与测试路径

内容 路径
文档源文件 docs/data/material/components/stack/stack.md
演示示例(基础/方向/分隔/响应式/flex gap/零宽) docs/data/material/components/stack/
Material UI 封装 packages/mui-material/src/Stack/Stack.js
类型定义 packages/mui-material/src/Stack/Stack.d.ts
核心实现(createStack 工厂与 style 函数) packages/mui-system/src/Stack/createStack.tsx
System 版本 Stack packages/mui-system/src/Stack/Stack.tsx
工具类 packages/mui-material/src/Stack/stackClasses.ts
测试用例 packages/mui-material/src/Stack/Stack.spec.tsxpackages/mui-system/src/Stack/Stack.test.js

小结

Stack 用最小的 API 面(direction + spacing + divider + useFlexGap)覆盖了绝大多数一维布局需求:默认纵向排列,spacing 通过主题 spacing 缩放,divider 自动在直接子元素间插入分隔元素,响应式对象可按断点切换方向与间距。理解源码后,两条实践建议值得记住:其一,若子元素需要自定义 margin,优先启用 useFlexGap(或按主题 defaultProps 全局启用);其二,在 direction="row" 中放置长文本/noWrap 内容时,给相关项补上 minWidth: 0 以避免 flex min-width: auto 造成的溢出。

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