Ant Design Space 组件实战完全指南:统一间距布局、Compact 紧凑模式与源码级实现解析
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
本文基于 ant-design 官方组件文档(components/space/index.en-US.md)展开,并结合 components/space 下的源码、样式与 demo 深入剖析。读完你可以完全掌握 Space 组件的全部 API 语义、与 Flex 的选型差异、间距(gap)的底层实现机制,以及 Space.Compact / Space.Addon 在表单紧凑场景下的正确用法。
何时使用 Space(When To Use)
Space 是 Ant Design 提供的"间距容器"组件,官方文档给出的使用场景很明确:
- 避免子组件"贴在一起"(clinging together),为其统一设置间距;
- 当子表单组件需要**紧凑连接、边框合并(border collapsed)**时,使用
Space.Compact(自antd@4.24.0起支持)。
典型示例见 base.tsx:在一个Space中混排Button、Upload、Popconfirm等组件,不需要手动为每个元素加margin,间距自动统一:
import { UploadOutlined } from '@ant-design/icons'; import { Button, Popconfirm, Space, Upload } from 'antd'; const App: React.FC = () => ( <Space> Space <Button type="primary">Button</Button> <Upload> <Button icon={<UploadOutlined />}>Click to Upload</Button> </Upload> <Popconfirm title="Are you sure delete this task?" okText="Yes" cancelText="No"> <Button>Confirm</Button> </Popconfirm> </Space> );与 Flex 组件的差异:如何正确选型
文档明确了两者的分工,这是布局选型时最容易混淆的地方:
- Space:用于设置行内元素(inline elements)之间的间距。它会给每个子元素包裹一层 wrapper(即下文
Item组件渲染的.ant-space-item),用于内联对齐。适合在横向/纵向上对多个子元素做等距排列。 - Flex:用于设置块级元素(block-level elements)的布局。它不增加 wrapper 元素,适合做子元素的垂直/水平方向的整体布局,提供更强的灵活性与控制力。
一句话总结:排间距用 Space,做布局用 Flex。Space 内部依赖 CSSinline-flex+gap实现,而 Flex 更接近裸 CSS Flexbox 的能力面。两者虽然都有vertical/wrap等外观相似的概念,但语义定位不同。
核心 API 全解析
Space 的所有子组件共享一份 Common props 约定。以下是文档列出的完整属性表及源码佐证。
Space 主组件属性
| Property | Description | Type | Default | Version | 全局配置支持 |
|---|---|---|---|---|---|
| align | 对齐方式 | start|end|center|baseline | -(横向布局下实际按center处理) | 4.2.0 | × |
| classNames | 为组件内各语义结构定制 class,支持对象或函数 | Record<SemanticDOM, string>|(info: { props: SpaceProps }) => Record<SemanticDOM, string> | - | 5.6.0 | 5.6.0 |
布局方向(已废弃,改用orientation) | vertical|horizontal | horizontal | 4.1.0 | × | |
| orientation | 布局方向 | vertical|horizontal | horizontal | - | × |
| size | 间距尺寸 | Size | Size[] | small | 4.1.0(数组:4.9.0) | 5.6.0 |
分隔符(已废弃,改用separator) | ReactNode | - | 4.7.0 | × | |
| separator | 子元素间的分隔内容 | ReactNode | - | - | × |
| styles | 为各语义结构定制内联样式,支持对象或函数 | Record<SemanticDOM, CSSProperties>|(info: { props: SpaceProps }) => Record<SemanticDOM, CSSProperties> | - | 5.6.0 | 5.6.0 |
| vertical | 垂直排布;与orientation同时配置时以orientation为准 | boolean | false | - | × |
| wrap | 自动换行(仅horizontal下生效) | boolean | false | 4.9.0 | × |
Size 类型
'small' | 'middle' | 'large' | numbersize既可以是预置档位,也可以直接传数字(像素值),还支持传[水平间距, 垂直间距]二元数组分别控制两个方向——这在wrap换行场景中尤其有用。
在源码 index.tsx 中可以看到,size传入数组时会被拆解为horizontalSize与verticalSize两个维度:
const [horizontalSize, verticalSize] = Array.isArray(size) ? size : ([size, size] as const); const isPresetVerticalSize = isPresetSize(verticalSize); const isPresetHorizontalSize = isPresetSize(horizontalSize); const isValidVerticalSize = isValidGapNumber(verticalSize); const isValidHorizontalSize = isValidGapNumber(horizontalSize);间距的两种渲染通道
从 style/index.ts 与 index.tsx 的实现可以提炼出两条间距渲染路径,理解后能避免"间距不生效"的困惑:
- 预置档位走 CSS class:当
size为small/middle/large时,组件生成形如ant-space-gap-row-small、ant-space-gap-col-large的语义 class,样式表再将其映射为对应 token 值的rowGap/columnGap(见 genSpaceGapStyle)。 - 数字值走内联 CSS gap:当
size为数字(非预置档位)时,直接在根元素内联样式上写入columnGap与rowGap:
const gapStyle: React.CSSProperties = {}; if (wrap) { gapStyle.flexWrap = 'wrap'; } if (!isPresetHorizontalSize && isValidHorizontalSize) { gapStyle.columnGap = horizontalSize; } if (!isPresetVerticalSize && isValidVerticalSize) { gapStyle.rowGap = verticalSize; }注意 gapSize.ts 中isValidGapNumber会故意忽略 0 值,注释说明理由:CSSgap属性的默认值就是 0,用户传入 0 时可以直接忽略,无需渲染冗余样式。
预置档位对应的 token 映射在 style/index.ts:
spaceGapSmallSize: token.paddingXS, // small → 4px(取决于主题) spaceGapMiddleSize: token.padding, // middle → 一般 16px spaceGapLargeSize: token.paddingLG, // large → 一般 24px方向属性:orientation / vertical / direction 的优先级
自 API 演进以来,方向控制经历了direction(4.1.0)→vertical→orientation的变迁。三者同时出现时以何为准?源码抽出了公共 hook useOrientation.ts:
export const useOrientation = (orientation, vertical, legacyDirection) => { return useMemo(() => { const validOrientation = isValidOrientation(orientation); if (validOrientation) { mergedOrientation = orientation; // 1. orientation 最优先 } else if (typeof vertical === 'boolean') { mergedOrientation = vertical ? 'vertical' : 'horizontal'; // 2. 其次 vertical } else { mergedOrientation = isValidOrientation(legacyDirection) ? legacyDirection : 'horizontal'; // 3. 兜底 direction } return [mergedOrientation, mergedOrientation === 'vertical']; }, [legacyDirection, orientation, vertical]); };优先级结论:orientation>vertical>direction(默认horizontal)。这与文档中"vertical与orientation同时配置时优先orientation"的描述一致。
此外,开发模式下 index.tsx 会对已废弃的direction、split属性发出 deprecation warning,提示分别改用orientation、separator;Compact同样在 Compact.tsx 中提示direction已废弃。
实战场景一:垂直排列
垂直堆叠多个块级内容(如 Card 列表)时使用orientation="vertical"(或vertical),对应 demo vertical.tsx。当需要子元素占满整行宽度时,可配合style={{ display: 'flex' }}覆盖根节点默认的inline-flex行为:
import { Card, Space } from 'antd'; const App: React.FC = () => ( <Space orientation="vertical" size="medium" style={{ display: 'flex' }}> <Card title="Card" size="small"> <p>Card content</p> </Card> <Card title="Card" size="small"> <p>Card content</p> </Card> </Space> );从样式层看,垂直方向由 genSpaceStyle 中的&-vertical { flexDirection: column }实现。
实战场景二:自定义与分方向间距(size 数组)
size支持数字与预置档位混排,demo size.tsx 演示了通过Radio.Group+Slider动态切换预置档位与自定义像素值的交互。
当同时存在水平、垂直两种间距需求时使用数组,例如 demo wrap.tsx:
<Space size={[8, 16]} wrap> {Array.from({ length: 20 }).map((_, index) => ( <Button key={index}>Button</Button> ))} </Space>size={[8, 16]}表示水平间距 8px、垂直间距(换行后的行距)16px;wrap令超宽子元素自动换行。文档明确wrap仅在水平(horizontal)方向下有效。
demo gap-in-line.tsx 更进一步展示了当容器宽度恰好小于一行所需宽度时(可通过width精确微调),子元素如何逐列换行——这正是 CSSgap(而非传统 margin)实现的特性之一,调试时可参考该 demo 中307 / 310像素的临界值差异。
实战场景三:对齐控制(align)
对齐方向支持start/end/center/baseline,demo align.tsx 用行内文本、按钮与高矮不一的块状元素演示四种对齐差异。注意源码中有一个"隐形默认值":在横向布局且未显式传入align时,组件默认按center处理(index.tsx):
const mergedAlign = align === undefined && !mergedVertical ? 'center' : align;样式层通过&${componentCls}-align-center/start/end/baseline分别映射align-items的取值(style/index.ts)。所以若想在水平布局下子元素顶部对齐,必须显式传align="start"。
实战场景四:分隔符 separator
当需要在子元素之间渲染统一的连接符(如竖分割线、圆点)时使用separator(旧版split已废弃)。demo separator.tsx:
import { Divider, Space, Typography } from 'antd'; const App: React.FC = () => ( <Space separator={<Divider vertical />}> <Typography.Link>Link</Typography.Link> <Typography.Link>Link</Typography.Link> <Typography.Link>Link</Typography.Link> </Space> );separator的实现细节很考究。首先,新旧属性在 index.tsx 合并:const mergedSeparator = separator ?? split;。其次,分隔符不会出现在最后一个可渲染子元素之后——Item.tsx 从SpaceContext中读取latestIndex,只有index < latestIndex时才渲染<span class="ant-space-item-separator">:
const { latestIndex } = React.useContext(SpaceContext); if (!isReactRenderable(children)) { return null; // 空子节点整体跳过 } return ( <> <div className={className} style={style}>{children}</div> {index < latestIndex && separator && ( <span className={`${prefix}-item-separator`}>{separator}</span> )} </> );latestIndex在 index.tsx 中由父组件计算并放入SpaceContext,其逻辑是"最后一个可渲染(react renderable)子元素的索引",从而保证遇到null、空数组等不可渲染子项时分隔符位置依然正确。
Space.Compact:表单组件紧凑连接
当若干表单子组件需要无间距紧贴、相邻边框合并为一条(例如地址输入框 + 选择器 + 按钮拼成工具条)时,使用Space.Compact(antd@4.24.0+)。官方 demo compact.tsx 展示了十余种混合形态。
支持的子组件
文档明确Space.Compact内置支持以下组件(它们内部消费了 Compact 提供的上下文):
- Button
- AutoComplete
- Cascader
- DatePicker
- Input / Input.Search
- InputNumber
- Select
- TimePicker
- TreeSelect
Space.Compact 属性
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| block | 是否占满父容器宽度 | boolean | false | 4.24.0 |
布局方向(已废弃,改用orientation) | vertical|horizontal | horizontal | 4.24.0 | |
| orientation | 布局方向 | vertical|horizontal | horizontal | - |
| vertical | 是否垂直排布,与orientation并存时以orientation为准 | boolean | false | - |
| size | 子组件尺寸 | large|medium|small | medium | 4.24.0 |
block会生成ant-space-compact-block样式,由默认的inline-flex切换为占满宽度的display: flex; width: 100%(见 compact.ts 样式)。size的默认档位medium与普通组件的middle语义等同,样式表中两者均被视作同一档(&-gap-row-medium, &-gap-row-middle)。
Compact 的边框合并原理
从源码 Compact.tsx 可以看出,Compact 并不直接改动子组件样式,而是通过React Context广播"位置身份":
export const SpaceCompactItemContext = React.createContext(null); childNodes.map((child, i) => ( <CompactItem key={child?.key || `${prefixCls}-item-${i}`} compactSize={mergedSize} compactDirection={mergedOrientation} isFirstItem={i === 0 && (!compactItemContext || compactItemContext?.isFirstItem)} isLastItem={i === childNodes.length - 1 && (!compactItemContext || compactItemContext?.isLastItem)} > {child} </CompactItem> ));各受支持组件通过 useCompactItemContext 读取该 Context,得到自身是否为第一个/最后一个子项、紧凑方向与尺寸,并拼接出诸如ant-input-compact-first-item、ant-input-compact-last-item的 class。样式规则再据此削掉首尾的圆角、合并相邻边框(borderInlineEndWidth: 0等),从而视觉上形成"一个整体控件"。同时,Compact 支持嵌套(外层 Compact 会向子 Compact 传递isFirstItem/isLastItem的延续判断),compact-nested.tsx就是该场景的调试 demo。
若某个子组件不希望被紧凑样式影响,可在外层用 NoCompactStyle 将 Context 置空,隔离样式传播。
Compact 应用示例(紧凑表单组合)
import { Button, Input, Select, Space } from 'antd'; <Space.Compact block> <Select allowClear defaultValue="Zhejiang" options={[ { label: 'Zhejiang', value: 'Zhejiang' }, { label: 'Jiangsu', value: 'Jiangsu' }, ]} /> <Input style={{ width: '50%' }} defaultValue="Xihu District, Hangzhou" /> </Space.Compact>纯按钮组的紧凑工具条见 compact-buttons.tsx(图标按钮 + Tooltip + Dropdown 混合、禁用态按钮混排);垂直方向的紧凑连接(例如窄屏下的堆叠表单)参考 compact-button-vertical.tsx,它依赖&{componentCls}-vertical { flexDirection: column }将排布切为纵向。此外两个调试 demo compact-debug.tsx(Input addon 边界)与 compact-nested.tsx(嵌套 Compact)记录了历史上容易出问题的边界形态。
Space.Addon:紧凑布局的自定义单元
自antd@5.29.0起新增Space.Addon,用于在紧凑布局中创建自定义"单元格"(类似 Input 前后缀的外置版,常用来插入$、¥、kg等附加文本),并保持与 Compact 组件的圆角、边框、尺寸、状态一致。
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| children | 自定义内容 | ReactNode | - | 5.29.0 |
Addon 的形态能力远超普通文本:从 Addon.tsx 的 props 看,它支持variant(outlined/filled/borderless/underlined,默认outlined)、disabled、status(error/warning)等表单化语义属性,内部会复用useCompactItemContext读取紧凑位置,并调用getStatusClassNames挂接状态样式。样式定义见 style/addon.ts:不同尺寸档位(large/small)联动圆角与字号、不同 variant 联动背景与边框变量(通过genCssVar生成 CSS 变量addon-border-color、addon-background等)。
Addon 同时可被 ConfigProvider theme 定制——demo component-token.tsx 展示了通过components: { Addon: { colorText: 'blue' } }改写 Addon 文本颜色:
import { Button, ConfigProvider, Space } from 'antd'; <ConfigProvider theme={{ components: { Addon: { colorText: 'blue' }, }, }} > <Space.Compact> <Space.Addon>Addon</Space.Addon> <Button type="primary">Button</Button> </Space.Compact> </ConfigProvider>组合示例(Input+Space.Addon插入货币符号 +InputNumber)见 compact.tsx。
Semantic DOM:语义化定制 classNames / styles
自 5.6.0 起,classNames与styles支持按语义结构精细化定制。Space 的语义 DOM 分为三部分(见 demo _semantic.tsx):
- root:根元素,承载 flex 布局、间距 gap、对齐、换行等容器基础样式;
- item:包裹每个子元素的内层
div.ant-space-item,实现内联对齐的包装; - separator:子元素之间的分隔符元素。
const classNamesObject: SpaceProps['classNames'] = { root: 'demo-space-root', item: 'demo-space-item', separator: 'demo-space-separator', };两者都既支持静态对象,也支持基于当前 props 的函数式返回,例如 demo style-class.tsx 中按orientation或size切换样式:
const classNamesFn: SpaceProps['classNames'] = (info) => info.props.orientation === 'vertical' ? { root: 'demo-space-root--vertical' } : { root: 'demo-space-root--horizontal' }; const stylesFn: SpaceProps['styles'] = (info) => info.props.size === 'large' ? { root: { backgroundColor: '#e6f7ff', padding: 8 } } : { root: { backgroundColor: '#fff7e6' } }; <Space styles={stylesFn} classNames={classNamesFn} separator="•"> <Button>Button 1</Button> <Button>Button 2</Button> </Space>在 index.tsx 的实现中,useComponentConfig('space')会先读取ConfigProvider注入的上下文 classNames/styles,再经useMergeSemantic与组件自身传入值合并,最终item的 class 统一拼为ant-space-item + mergedClassNames.item,渲染在每一个Item的包装div上。Root class 生成逻辑见 index.tsx,其中还包括 RTL(direction === 'rtl'时追加ant-space-rtl)、对齐、gap 档位等多组条件 class。
主题与 Design Token
Space 组件本身通过空对象ComponentToken参与主题体系(style/index.ts),实际可定制的是经由 alias token 派生的三个 gap token:
spaceGapSmallSize←paddingXSspaceGapMiddleSize←paddingspaceGapLargeSize←paddingLG
也就是说,Space 的预置档位间距跟随全局padding系 token 联动:在ConfigProvider的theme.token中调整padding/paddingLG/paddingXS,即可同时影响 Space 的small/middle/large档位间距。Addon 单元则拥有独立的组件级 token(如colorText、边框与背景变量),可按需在theme.components.Addon下覆盖。完整的 token 表格以官方站点<ComponentTokenTable component="Space">渲染结果为准。所有空间相关样式均通过genStyleHooks生成,且显式关闭了resetStyle(resetStyle: false,Space 不应用额外字体重置,参见 style/index.ts 对 issue #40315 的说明)。
测试与验证
仓库在 components/space/tests下提供了覆盖上述行为的测试,可作为"源码行为"的交叉验证入口:
- gap.test.ts:验证预置档位 class 与数字
size的内联gap是否按预期生成; - space-compact.test.tsx:验证 Compact 的 block、垂直/水平方向、首尾项 context 语义;
- index.test.tsx:验证默认对齐、separator 渲染、空子节点、尺寸合并等主组件行为;
- a11y.test.ts:可访问性(无障碍)相关断言。
连同 semantic.test.tsx(Semantic DOM 定制)与快照目录中的demo.test.tsx.snap,共同守护 Space 的视觉与逻辑稳定性。
小结:一套 API 覆盖"间距 + 紧凑连接"两大诉求
回到开篇的两个核心场景,可以形成清晰的实践结论:
- 等距排列组件(按钮组、工具栏、卡片堆叠)——用基础
Space,size支持预置档位/数字/二维数组,wrap负责换行,separator负责元素间连接符,align控制纵向对齐,classNames/styles按语义节点做细粒度定制; - 表单控件紧凑拼装(输入框 + 下拉 + 按钮连成整体控件)——用
Space.Compact合并边框并统一尺寸,配合Space.Addon插入带状态感知的自定义单元; - 真正需要掌控子元素布局比例时——切换为 Flex 组件,二者互补而非替代。
从源码视角看,Space 的全部核心能力其实建立在三个设计支点上:inline-flex + gap的现代间距方案、React Context(SpaceContext与SpaceCompactItemContext)驱动的"谁该被隔开 / 谁在边缘"判定,以及"wrapper Item"的内联对齐包装模型。理解这三点,无论自定义样式还是排查间距异常都会事半功倍。
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考