首页
/ Material UI Stack 组件深入解析:一维布局、响应式间距与 Flexbox Gap 实现原理

Material UI Stack 组件深入解析:一维布局、响应式间距与 Flexbox Gap 实现原理

2026-09-06 17:44:09作者:曹令琨Iris

Stack 是 MUI System 中用于将直接子元素沿垂直或水平轴排列的容器组件,支持在子元素之间注入间距与分隔元素。本文以 Stack 官方文档源文件 为主体,结合 createStack.tsx 的源码实现,完整讲解 spacingdirectiondivideruseFlexGap 等核心属性的用法与底层 CSS 生成逻辑,以及默认间距实现的两个已知限制与规避方案。

Stack 是什么:一维布局容器

Stack 组件管理其直接子元素在垂直或水平轴上的布局,并可在每对相邻子元素之间提供可选的间距(spacing)与分隔符(divider)。

官方文档中特别强调了一个选型原则:

  • Stack 适合一维布局(要么水平、要么垂直);
  • 当需要同时处理垂直与水平两个维度的布局时,应优先使用 Grid。

从源码结构看,Stack 是 @mui/system 包中的基础组件,通过 createStack() 工厂函数创建,Stack.tsx 中仅一行 const Stack = createStack();,并带有 'use client' 指令,表明它是面向客户端渲染的 React 组件。

安装与引入

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

Stack 是一个通用容器组件,包裹住所有需要排列的元素。在 Material UI 体系中,@mui/material 也对外提供了 Stack 的入口(见 Stack.js),便于与 Material 组件库混用。

Basics:spacing 属性控制子元素间距

使用 spacing 属性控制子元素之间的空间。间距值可以是任意数字(含小数)或字符串,例如 spacing={2}spacing="1.5"。该值会通过主题的 theme.spacing() 辅助函数转换为具体的 CSS 长度值,因此它继承主题的间距标度(spacing scale)——这是 Stack 间距与 sxm/p 系统属性保持一致的原因。

官方基础示例(BasicStack.tsx)如下:

import Box from '@mui/system/Box';
import Stack from '@mui/system/Stack';
import { styled } from '@mui/system';

const Item = styled('div')(({ theme }) => ({
  backgroundColor: '#fff',
  padding: theme.spacing(1),
  textAlign: 'center',
  borderRadius: 4,
}));

export default function BasicStack() {
  return (
    <Box sx={{ width: '100%' }}>
      <Stack spacing={2}>
        <Item>Item 1</Item>
        <Item>Item 2</Item>
        <Item>Item 3</Item>
      </Stack>
    </Box>
  );
}

<Box sx={{ width: '100%' }}> 只是用于撑满演示区宽度的外壳,核心是 <Stack spacing={2}> 包裹的三个 Item

Stack vs. Grid

Stack 只关心一维布局,而 Grid 处理二维布局。Stack 的默认方向是 column,即子元素纵向堆叠。

Direction:控制排列方向

默认情况下 Stack 以 column(列)方式纵向排列子元素。使用 direction 属性可以让子元素横向(行)排列:

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

该示例对应 DirectionStack.tsxdirection 的完整取值有四种:rowrow-reversecolumncolumn-reverse,直接映射到 CSS 的 flex-direction

源码级实现:在 createStack.tsxstyle 函数中,direction 通过 resolveBreakpointValues 解析后经 handleBreakpoints 输出为各断点下的 flexDirection 声明,根节点始终带有 display: 'flex'

let styles = {
  display: 'flex',
  flexDirection: 'column',
  ...handleBreakpoints(
    { theme },
    resolveBreakpointValues({
      values: ownerState.direction,
      breakpoints: theme.breakpoints.values,
    }),
    (propValue) => ({ flexDirection: propValue }),
  ),
};

组件默认值在 createStack.tsx 中解构确认:component = 'div'direction = 'column'spacing = 0useFlexGap = false

Dividers:在子元素之间插入分隔符

使用 divider 属性可以在每一对相邻子元素之间插入一个 React 元素(DividerStack.tsx):

<Stack
  direction="row"
  divider={
    <Box
      component="hr"
      sx={(theme) => ({
        border: `1px solid ${'#fff'}`,
        ...theme.applyStyles('dark', {
          border: `1px solid ${'#262B32'}`,
        }),
      })}
    />
  }
  spacing={2}
>
  <Item>Item 1</Item>
  <Item>Item 2</Item>
  <Item>Item 3</Item>
</Stack>

源码级实现:分隔逻辑位于 createStack.tsxjoinChildren 函数——它把 children 展平为数组,用 reduce 依次输出每个子节点,并在除最后一个以外的每个子节点之后 React.cloneElement 一份分隔符,key 为 separator-${index}

function joinChildren(children, separator) {
  const childrenArray = React.Children.toArray(children).filter(Boolean);

  return childrenArray.reduce((output, child, index) => {
    output.push(child);
    if (index < childrenArray.length - 1) {
      output.push(React.cloneElement(separator, { key: `separator-${index}` }));
    }
    return output;
  }, []);
}

组件渲染时对 divider 做了三元判断(createStack.tsx):传了 divider 就走 joinChildren 插桩,否则原样输出 children。注意 divider 接收的是单个 React 节点,组件负责克隆它,而非函数。

Responsive values:按断点切换 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:小屏下三项目纵排且间距为 1,sm 断点起变为横排、间距 2,md 断点起间距扩大到 4。

源码级的一个细节:当 spacing 为对象而 direction 是字符串时,style 函数会遍历 direction 对象中缺失的断点,并继承上一个断点的方向值createStack.tsx)。这保证了在混合响应式写法下(例如只在 sm 以上指定 spacing)方向不会“断档”——缺失断点沿用上文的 flex-direction,缺省回退为 column

Flexbox gap:useFlexGap 属性

要改用 CSS 原生 flexbox gap 来实现间距,可将 useFlexGap 属性设为 trueFlexboxGapStack.tsx):

<Box sx={{ width: 200 }}>
  <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>
</Box>

该写法移除了默认实现(基于 CSS 相邻选择器)的已知限制(见下文 Limitations)。但 CSS flexbox gap 在部分浏览器中尚未被完全支持,官方建议启用前先查看浏览器兼容性数据(文档中指向了 caniuse 上 "flex gap" 的支持率统计)。

两种实现的源码对比createStack.tsx):

const styleFromPropValue = (propValue, breakpoint) => {
  if (ownerState.useFlexGap) {
    return { gap: getValue(transformer, propValue) };
  }
  return {
    // 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.
    '& > :not(style):not(style)': {
      margin: 0,
    },
    '& > :not(style) ~ :not(style)': {
      [`margin${getSideFromDirection(
        breakpoint ? directionValues[breakpoint] : ownerState.direction,
      )}`]: getValue(transformer, propValue),
    },
  };
};
  • useFlexGaptrue 时:只输出 gap,值由 createUnarySpacing(theme) 生成的转换器(transformer)结合主题标度换算;
  • 默认实现:先把所有直接子元素(排除 <style> 标签,即 :not(style))的 margin 重置为 0,再用 ~ 通用兄弟选择器给非首个子元素加上沿主轴方向的 margin(如 marginLeft/marginTop)。

方向到 margin 侧的映射由 getSideFromDirection 完成:

direction 注入的 margin 侧
row margin-left
row-reverse margin-right
column margin-top
column-reverse margin-bottom

此外,属性文档中说明:可以通过主题的 default props 配置在全局层面启用 useFlexGap,而无需在每个 Stack 上重复书写(见 Stack.tsx 的 PropTypes 注释)。

Interactive demo:交互式探索

文档还提供了一个可交互演示(InteractiveStack.tsx),允许在界面中实时切换不同 directionspacing 等配置并观察视觉结果,适合在需要向团队演示 Stack 行为差异时使用。

sx prop:任意实例的快速定制

使用 sx 属性可以对任意 Stack 实例进行快速定制。sx 是一个 CSS 超集,可访问 MUI System 暴露的全部样式函数与主题感知属性。例如居中排列子项:

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

由于 Stack 根节点本身就是 display: flexalignItemsjustifyContentflexWrap 等 flex 容器属性都能直接在 sx 中使用。

Limitations:默认实现的两个已知限制

限制一:子元素的 margin 会被忽略

默认实现(useFlexGap={false})不支持自定义子元素的 margin。例如:

<Stack>
  <button style={{ marginTop: '30px' }}>...</button>
</Stack>

上述 button 上的 marginTop 会被忽略。这不是 bug,而是实现的必然结果:默认实现对所有直接子元素强制执行 margin: 0(见上文 & > :not(style) 重置规则),以收回子元素对间距的控制权、避免双重间距。

官方给出的解决方案:将 useFlexGap 设为 true,切换到 CSS flexbox gap 实现。文档中还引用了一份社区 RFC(GitHub issue #33754)供深入讨论该限制的背景。

限制二:white-space: nowrap 导致的定位冲突

flex 项目初始的 min-widthauto。当子元素使用 white-space: nowrap; 时,会产生定位冲突。可用如下代码复现:

<Stack direction="row">
  <span style={{ whiteSpace: 'nowrap' }}>

要让项目保持在容器内部,需要设置 min-width: 0

<Stack direction="row" sx={{ minWidth: 0 }}>
  <span style={{ whiteSpace: 'nowrap' }}>

Anatomy:DOM 结构

Stack 组件由单一的根 <div> 元素构成,不产生额外的包裹层:

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

类名 MuiStack-rootgenerateUtilityClass('MuiStack', 'root') 生成(组件名 MuiStack、slot root),与 createStack.tsxname: 'MuiStack', slot: 'Root' 的 styled 配置一致。

属性总览

综合 StackProps.ts 的类型定义与 Stack.tsx 的 PropTypes,Stack 的核心属性如下:

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

源码导读与延伸阅读

内容 路径
Stack 官方文档源文件 stack.md
核心工厂实现(style 函数、joinChildren、方向映射) createStack.tsx
类型定义(StackBaseProps / StackTypeMap) StackProps.ts
@mui/system 导出入口与 PropTypes Stack.tsx
@mui/material 入口 Stack.js
单元测试 Stack.test.js
各官方示例源码 BasicStack.tsxDirectionStack.tsxDividerStack.tsxResponsiveStack.tsxFlexboxGapStack.tsxInteractiveStack.tsx

实践小结:一维排列、需要主题化间距与断点自适应时选择 Stack;需要二维网格时改用 Grid;当子元素必须保留自身 margin,或对 margin 重置敏感时,启用 useFlexGap 前先确认目标浏览器对 flex gap 的支持程度。

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