间距系统工程:从尺度数学到空间语义的层级构建
间距系统工程:从尺度数学到空间语义的层级构建
本文是设计系统系列的第十篇。前九篇分别讨论了 Token 架构、风格光谱、动效系统、设计语言迁移、组件级微交互、页面叙事节奏、数据可视化组件、排版系统和色彩系统——间距在其中每篇都以"已知条件"出现:
--space-4、padding: 48px、gap: 16px……但间距自身的生成规则、语义分层和组件消费策略从未被独立拆解。本文补上这块拼图:间距不是"留点空白",而是一套从尺度数学到空间语义的工程体系——它决定了界面中每个元素的呼吸量、每段关系的亲密感、每层结构的清晰度。
一、间距为什么需要"工程"
大多数前端开发者对间距的处理方式是这样的:设计师给了 Figma 稿,开发者照着稿里的数值写 padding: 24px、margin-bottom: 16px、gap: 8px。如果没有设计稿,就"凭感觉"——标题下面留大一点,正文之间留小一点,按钮里面留适中。
这种方式的问题在于:间距值之间没有结构关系。24px、16px、8px 是独立选择的——当你把卡片内边距从 24px 改为 20px 时,你不知道卡片内的标题与正文之间的 16px 是否也需要调整,因为它们之间没有尺度约束。
间距系统的核心思想与排版(第八篇)、色彩(第九篇)同构:所有间距值不是独立数字,而是一个尺度系统的衍生值。尺度定义了间距的生成规则(数学比例),语义定义了间距的角色映射(内/外/间隙),组件定义了间距的消费纪律(组件只引用语义 Token)。
1.1 间距系统的三个层次
层次 1: 尺度层(Scale) — 间距值的数学生成规则
层次 2: 语义层(Semantic) — 间距与空间角色的映射
层次 3: 组件层(Component) — 语义间距在组件中的消费规则
- 尺度层解决"有哪些间距值可用"——按等差或等比规则生成的一组原子间距
- 语义层解决"这个间距表示什么空间关系"——同一套尺度在不同场景下映射为不同语义
- 组件层解决"这个组件用哪个语义间距"——组件永远不直接引用原子间距
核心纪律:组件代码中禁止出现裸间距值。一旦组件里写了 padding: 20px,这个组件就脱离了间距系统——当尺度调整时它不会跟随,当响应式缩放时它不会适配。
二、尺度层:间距的数学
2.1 等差 vs 等比——间距与排版的不同选择
第八篇讨论排版时,我论证了字号应该用等比数列(1.2 比例)——因为人眼对字号差异的感知是对数的。但间距的感知机制不同:人眼对间距差异的感知接近线性——8px 和 12px 的差异感知与 24px 和 28px 的差异感知大致相同(都是 4px 的绝对增量)。
这意味着间距尺度可以用等差数列——每级固定增量,而非等比的固定比例。
:root {
--space-0: 0;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;
}
注意序列的规律:前半段(0-6)是 4px 步长,后半段(6-24)是 8px 步长。这不是"不严格的等差"——而是分段等差,小间距用小步长,大间距用大步长。
为什么分段? 因为间距的用途随尺度变化:
| 尺度区间 | 步长 | 典型用途 | 感知精度要求 |
|---|---|---|---|
| 0-24px | 4px | 组件内部(padding、gap、margin) | 高——4px 的差异在组件内可感知 |
| 24-96px | 8px | section 间距、页面级留白 | 中——8px 的差异在页面级才可感知 |
如果在 24-96px 区间也用 4px 步长,会生成 28px, 32px, 36px, 40px, 44px, 48px, 52px, 56px, 60px, 64px——这些值在页面级间距中几乎无法区分(用户看不出 44px 和 48px 的区别),却增加了 Token 数量和选择负担。分段等差在"精度需求"和"系统简洁性"之间取了平衡。
2.2 基准值的选择:为什么是 4px
4px 是间距尺度的最小步长——所有间距值都是 4 的倍数。这个选择有三个理由:
理由一:4px 是视觉感知的下限。2px 的间距差异在某些屏幕上几乎不可见(特别是高 DPI 屏幕上的 2px 物理像素极小),4px 是"可靠可感知"的最小增量。如果尺度中有 2px 步长,开发者会在"2px 和 4px 之间"犹豫——而这个差异在大多数场景下不值得犹豫。
理由二:4px 与排版系统对齐。第八篇的排版系统中,基准行高是 24px(= 6 × 4px),基准字号是 16px(= 4 × 4px)。当间距尺度以 4px 为步长时,排版与间距自然对齐到同一个网格——这就是"垂直节奏"的数学基础(第八篇 7.2 节)。
理由三:4px 与组件尺寸对齐。常见的组件高度(按钮 36px、输入框 40px、徽章 24px)都是 4 的倍数。当间距和组件尺寸都基于 4px 网格时,组件在布局中的对齐天然整齐——不需要额外的 align-items: center 来"修正"半像素的错位。
2.3 间距尺度的完整定义
:root {
/* 组件级间距 — 4px 步长 */
--space-0: 0; /* 无间距 — 紧贴 */
--space-1: 4px; /* 微间距 — 图标与文字 */
--space-2: 8px; /* 小间距 — 相关元素组 */
--space-3: 12px; /* 中间距 — 卡片内元素 */
--space-4: 16px; /* 基准间距 — 段落、列表项 */
--space-5: 20px; /* 稍宽间距 — 卡片内边距 */
--space-6: 24px; /* 宽间距 — section 内元素 */
/* 页面级间距 — 8px 步长 */
--space-8: 32px; /* section 间距 */
--space-10: 40px; /* 大 section 间距 */
--space-12: 48px; /* section 内边距 */
--space-16: 64px; /* Hero 区域留白 */
--space-20: 80px; /* 大 Hero 留白 */
--space-24: 96px; /* 最大留白 */
}
命名规范:与排版系统一致,使用尺寸命名(--space-4)而非语义命名(--space-card-padding)。语义由组件层 Token 表达,尺度层只负责"有哪些值"。
为什么没有 --space-7(28px)? 因为 28px 落在 24px 和 32px 之间——在页面级间距中无法与两者区分。跳过 7 让尺度更干净:开发者不会在 24px 和 32px 之间犹豫"要不要用 28px"。尺度层的纪律不仅是"定义有哪些值",更是"定义没有哪些值"——排除模糊地带的选项,减少决策成本。
三、语义层:间距到空间关系的映射
3.1 间距的三种空间角色
间距在界面中承担三种不同的空间角色:
角色 1: 内间距(Inset) — 元素内部的留白(padding)
角色 2: 外间距(Stack) — 元素之间的垂直距离(margin / gap)
角色 3: 间隙(Inline) — 水平排列元素之间的距离(gap)
这三种角色看似都是"间距",但它们的语义完全不同:
- 内间距表达"这个元素的边界在哪里"——padding 越大,元素越"宽松"
- 外间距表达"这两个元素的关系有多远"——margin 越大,关系越"疏远"
- 间隙表达"这组元素是并列的"——gap 让元素保持关联但不重叠
关键区分:内间距是"元素自身的属性",外间距是"元素之间的关系",间隙是"组内的结构"。混淆这三种角色是间距混乱的根源——用 padding 表达"元素之间的关系",或用 margin 表达"元素内部的留白",都会导致间距系统失效。
3.2 内间距的语义分层
内间距(padding)按元素的空间层级分层:
| 语义 Token | 值 | 用途 | 空间层级 |
|---|---|---|---|
--inset-xs |
4px 8px | 徽章、标签 | 最紧凑 |
--inset-sm |
8px 12px | 小按钮、输入框 | 紧凑 |
--inset-md |
12px 16px | 常规按钮、列表项 | 标准 |
--inset-lg |
16px 24px | 卡片 | 宽松 |
--inset-xl |
24px 32px | 大卡片、面板 | 最宽松 |
--inset-hero |
48px 40px | CTA 卡片、Hero | 特殊 |
注意水平与垂直的不对称:--inset-md 是 12px 16px(垂直 12px,水平 16px)——水平内间距大于垂直内间距。这是因为:
- 文字的水平延展性:中英文文字在水平方向上自然延展,水平内间距需要更大以容纳文字宽度的变化
- 垂直方向的紧凑预期:用户对垂直方向的紧凑度更敏感——按钮的垂直内间距过大会让按钮看起来"膨胀",而水平内间距过大只让按钮"变长"
--inset-hero 的特殊处理:CTA 卡片和 Hero 区域的内间距是 48px 40px——垂直 48px 大于水平 40px,与常规内间距的方向相反。这是因为 Hero/CTA 是"全宽元素"——水平方向不需要更多内间距(内容已经居中),但垂直方向需要更多留白来产生"呼吸感"和"分量感"(第六篇:CTA 是最后的视觉高峰)。
3.3 外间距的语义分层
外间距(margin / gap)按元素之间的关系亲密度分层:
| 语义 Token | 值 | 关系 | 典型场景 |
|---|---|---|---|
--stack-xs |
4px | 紧贴 | 图标与标签 |
--stack-sm |
8px | 密切相关 | 标题与副标题 |
--stack-md |
16px | 相关 | 段落之间 |
--stack-lg |
24px | 同组不同项 | 卡片内元素组之间 |
--stack-xl |
32px | 不同组 | section 内卡片之间 |
--stack-2xl |
48px | 不同 section | section 之间 |
关系亲密度的映射规则:间距越大,关系越疏远。这不是审美判断——是格式塔心理学的接近性原则(Law of Proximity):人眼将距离近的元素视为"一组",距离远的元素视为"不同组"。间距系统利用这个原则,通过间距值来编码元素之间的关系结构。
实践验证:在指标卡片中,数字与标签之间用 --stack-xs(4px)——它们是"同一个信息单元";卡片内不同信息块之间用 --stack-md(16px)——它们是"同一卡片的不同维度";卡片之间用 --stack-xl(32px)——它们是"同一 section 的不同卡片"。用户不需要思考"这些元素是什么关系"——间距已经告诉了他们。
3.4 间隙的语义分层
间隙(gap)用于 flex/grid 布局中并列元素之间的距离:
| 语义 Token | 值 | 场景 |
|---|---|---|
--gap-xs |
4px | 图标组、标签组 |
--gap-sm |
8px | 按钮组、导航项 |
--gap-md |
16px | 卡片网格 |
--gap-lg |
24px | section 级布局 |
gap 与 margin 的选择:在 flex/grid 容器中,用 gap 而非 margin——gap 只在元素之间产生间距,不在首尾产生多余间距;margin 需要处理首尾元素的 margin 抵消(margin-left: 0 等)。gap 是"组内间距"的语义正确表达——它明确表示"这是并列元素之间的距离",而非"这个元素的外部留白"。
3.5 语义 Token 的完整定义
:root {
/* 内间距 */
--inset-xs: 4px 8px;
--inset-sm: 8px 12px;
--inset-md: 12px 16px;
--inset-lg: 16px 24px;
--inset-xl: 24px 32px;
--inset-hero: 48px 40px;
/* 外间距(垂直堆叠) */
--stack-xs: 4px;
--stack-sm: 8px;
--stack-md: 16px;
--stack-lg: 24px;
--stack-xl: 32px;
--stack-2xl: 48px;
/* 间隙(水平并列) */
--gap-xs: 4px;
--gap-sm: 8px;
--gap-md: 16px;
--gap-lg: 24px;
}
为什么三类语义 Token 的值有重叠? --stack-sm 和 --gap-sm 都是 8px——它们在尺度上相同,但在语义上不同。--stack-sm 表示"垂直方向上密切相关元素之间的距离",--gap-sm 表示"水平方向上并列元素之间的距离"。值相同是因为 8px 在两个方向上都适合"密切相关"的语义——但 Token 名字不同,因为当尺度调整时,两个方向可能需要不同的调整策略(如移动端垂直间距需要增加,但水平间距不需要)。
四、组件层:间距的消费纪律
4.1 组件间距的三问
组件选间距时回答三个问题:
- 这是内间距、外间距还是间隙?(inset / stack / gap)
- 关系亲密度或空间层级是什么?(xs / sm / md / lg / xl)
- 是否需要响应式调整?(移动端是否缩放)
以"卡片"为例:
- 卡片的 padding → 内间距,层级 lg →
--inset-lg(16px 24px) - 卡片内标题与正文之间 → 外间距,关系密切相关 →
--stack-sm(8px) - 卡片内正文与底部按钮之间 → 外间距,关系相关 →
--stack-md(16px) - 卡片网格中卡片之间 → 间隙,场景是卡片网格 →
--gap-md(16px)
四个间距值,四个不同的语义 Token——没有一个是"凭感觉"选的。
4.2 卡片的间距解剖
一张卡片从外到内的间距层次:
┌──────────────────────────────────────┐
│ ← --inset-lg (16px 24px) ──→ │ 卡片内边距
│ ┌────────────────────────────────┐ │
│ │ 标题 │ │ ← --stack-sm (8px) →
│ │ 副标题 │ │
│ │ ← --stack-md (16px) → │ │
│ │ 正文内容... │ │
│ │ ← --stack-lg (24px) → │ │
│ │ ┌──────┐ ┌──────┐ │ │ ← --gap-sm (8px) →
│ │ │ 按钮 │ │ 按钮 │ │ │
│ │ └──────┘ └──────┘ │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
← --gap-md (16px) → ← 卡片之间的间隙
间距的递减规律:从卡片外到卡片内,间距值递减——卡片间 16px → 卡片内边距 24px(水平)/16px(垂直)→ 卡片内元素组间 24px → 组内元素间 8-16px → 按钮间 8px。
为什么卡片内边距(24px)比卡片间间隙(16px)大? 因为卡片内边距是"卡片的呼吸量"——它定义了卡片内容的舒适度;卡片间间隙是"卡片的关系距离"——它定义了卡片之间的区分度。呼吸量需要大于关系距离——如果内边距小于间隙,卡片内容会"顶到"卡片边缘,视觉上"拥挤";而卡片之间虽然有 16px 的间隙,但因为内边距更大,每张卡片仍然有足够的"自我空间"。
4.3 按钮的间距解剖
按钮的间距比卡片更精密——因为按钮是界面中最频繁交互的元素,间距的微小差异直接影响点击体验:
.btn {
padding: var(--inset-md); /* 12px 16px */
}
.btn-sm {
padding: var(--inset-sm); /* 8px 12px */
}
.btn .icon + .text {
margin-left: var(--gap-xs); /* 4px */
}
按钮内边距的选择逻辑:
--inset-md(12px 16px):常规按钮——12px 的垂直内边距让按钮高度达到约 40px(12 + 16字号 + 12 = 40),符合 Apple HIG 的最小可点击区域(44px)的接近值--inset-sm(8px 12px):小按钮——用于空间受限的场景(如卡片内的辅助按钮),高度约 32px——低于 44px 的可点击区域推荐值,但通过cursor: pointer和视觉反馈补偿
图标与文字的 4px 间隙:按钮内图标与文字之间用 --gap-xs(4px)——这是"紧贴"的语义。图标和文字是"同一个按钮的两个部分",它们之间的关系是最亲密的——4px 足以让用户区分图标和文字,但不至于让它们"分离"。
为什么不用 8px? 8px 会让图标和文字之间产生"可感知的缝隙"——用户会看到"图标 + 空隙 + 文字",而非"图标文字"。4px 让图标和文字保持"一体感"——它们被感知为一个组合单元,而非两个独立元素。
4.4 section 的间距解剖
section 的间距涉及两个维度:section 自身的内边距和 section 之间的外间距:
.section {
padding: var(--space-12) 0; /* 48px 0 — section 内边距 */
}
.section + .section {
margin-top: var(--stack-2xl); /* 48px — section 间外间距 */
}
section 内边距 48px 的选择:
- 32px 太紧:section 内容与上下 section 的边界距离不够,用户感知不到"进入了新区域"
- 48px 适中:足够产生"区域感"——用户滚动到一个新 section 时,48px 的留白让他们"自然减速",感知到"这里是一个新的内容区域"
- 64px 太松:在效率工具中,64px 的 section 内边距会让页面过长——用户需要更多滚动才能看到下一个 section
section 间外间距 48px:与 section 内边距相同——这不是巧合。当两个 section 的内边距都是 48px 时,它们之间的总间距是 48px(上一个 section 的底部内边距)+ 48px(section 间外间距)+ 48px(下一个 section 的顶部内边距)= 144px。但实际视觉间距是上一个 section 内容底部到下一个 section 内容顶部的距离 = 48px + 48px = 96px(section 间外间距只在两个 section 的 padding 之间,不叠加)。
为什么 section 间外间距等于 section 内边距? 因为 section 的"边界"是由背景色变化定义的(第六篇:白→灰→白的交替),而非由间距定义的。section 间外间距的作用是确保两个 section 的背景色之间有足够的过渡空间——48px 确保用户在滚动时不会"突然"从一个背景色跳到另一个。
4.5 导航栏的间距解剖
导航栏是间距最精密的组件——因为导航项在水平方向上紧密排列,间距的微小差异会直接影响导航的"拥挤感":
.nav {
gap: var(--gap-xs); /* 4px — 导航项之间 */
}
.nav-item {
padding: var(--inset-sm); /* 8px 12px — 导航项内边距 */
}
导航项之间 4px 间隙:导航项之间的关系是"并列的同级选项"——4px 让它们保持"一组"的感觉,同时有足够的间距区分每个选项。如果用 8px,导航项会显得"分散"——用户可能感知不到它们是"同一组导航"。如果用 0,导航项会"粘"在一起——用户无法区分"这是一个长导航还是多个短导航"。
导航项内边距 8px 12px:垂直 8px 让导航项的高度约 32px(8 + 16字号 + 8)——比按钮矮,因为导航项不需要按钮那样的"可点击区域"权重。水平 12px 让导航项的文字与相邻导航项之间有足够的间距——12px + 4px(gap)= 16px 的总间距,与 --stack-md 的"相关"语义对齐。
五、间距与设计系统其他维度的关系
5.1 间距与排版
第八篇讨论了"垂直节奏"——所有垂直方向的间距应该是间距 Token 的整数倍。这里补充间距与排版的另一个交叉点:行高与间距的对齐。
当正文的行高是 24px(= --space-6)时,段落之间的间距应该是 24px 或 48px(1 倍或 2 倍行高)——而非 20px 或 32px。因为段落间距的作用是"在视觉上分隔两段文字"——如果段落间距等于一个行高,用户感知到"跳了一行";如果等于两个行高,用户感知到"跳了两行"——这两种感知都是"自然"的。但如果段落间距是 20px(不是行高的整数倍),用户会感知到"跳了不到一行"——这种"不完整"的跳跃让阅读节奏变得"不规律"。
.paragraph {
margin-bottom: var(--space-6); /* 24px = 1 倍行高 */
}
.section-content {
margin-bottom: var(--space-12); /* 48px = 2 倍行高 */
}
核心原则:行高是垂直节奏的"基本单位",所有垂直间距应该是行高的整数倍。当行高变化时(如响应式调整),间距也需要按比例调整——这就是为什么行高和间距都需要做成 Token:它们是联动的。
5.2 间距与色彩
间距与色彩的交叉点在表面层级(第九篇)——不同的表面层级(surface / surface-2 / surface-3)需要不同的内边距来表达"空间深度":
| 表面层级 | 内边距 | 空间语义 |
|---|---|---|
--surface(卡片) |
--inset-lg(16px 24px) |
浮起的独立空间 |
--surface-2(section 背景) |
section padding 48px | 区域级的空间 |
--surface-3(输入框底) |
--inset-sm(8px 12px) |
嵌套的紧凑空间 |
规律:表面层级越高(越"浮起"),内边距越大——因为"浮起"的元素需要更多的"自我空间"来强调它的独立性。嵌套的表面层级(surface-3)内边距最小——因为嵌套元素是"在另一个空间内部",不需要额外的自我空间。
5.3 间距与动效
间距与动效的交叉点在位移幅度(第五篇)——入场动画的位移(20px)和 hover 位移(2px)都应该使用间距 Token:
/* 入场动画 */
.hero {
transform: translateY(var(--space-5)); /* 20px */
}
/* hover 反馈 */
.card:hover {
transform: translateY(calc(-1 * var(--space-1))); /* -4px → 但实际用 2px */
}
注意:hover 位移是 2px,不在间距尺度中(最小是 4px)。这是因为 2px 是"可感知但不夸张"的交互位移甜蜜点——它太小了,不值得做成间距 Token。间距尺度的下限是 4px——低于 4px 的位移属于"微交互参数",不属于"间距系统"。
入场位移 20px:在间距尺度中对应 --space-5。使用间距 Token 的好处是:当尺度调整时(如移动端缩小间距),入场位移自动跟随——保持位移与间距的比例关系。
5.4 间距与页面节奏
第六篇讨论了页面叙事节奏——信息密度的高低交替。间距是信息密度的直接控制变量:
| Section | 信息密度 | 间距策略 | 效果 |
|---|---|---|---|
| Hero | 中 | 大内边距(64px)+ 中元素间距 | “呼吸感”——不拥挤 |
| Dashboard | 高 | 小内边距(16px)+ 小元素间距 | “紧凑感”——信息密集 |
| Metrics | 中 | 中内边距(24px)+ 中元素间距 | “平衡感”——不松不紧 |
| CTA | 低 | 大内边距(48px)+ 大元素间距 | “留白感”——极简 |
间距是信息密度的"调节阀"——当间距增大时,同样的内容占据更多空间,信息密度降低;当间距减小时,内容更紧凑,信息密度升高。第六篇的"密度交替"策略(中→高→中→低)在间距层面的实现就是:Hero 用大间距 → Dashboard 用小间距 → Metrics 用中间距 → CTA 用大间距。
六、响应式间距
6.1 移动端的间距缩放
移动端的间距需要系统性缩放——但不是"所有间距等比缩小",而是分层缩放:
@media (max-width: 640px) {
:root {
/* 页面级间距 — 大幅缩小 */
--space-12: 32px; /* 48 → 32 */
--space-16: 48px; /* 64 → 48 */
--space-20: 48px; /* 80 → 48 */
--space-24: 48px; /* 96 → 48 */
/* 组件级间距 — 微调或不调 */
--space-4: 16px; /* 保持 */
--space-6: 20px; /* 24 → 20 */
}
}
分层缩放的逻辑:
- 页面级间距大幅缩小:移动端屏幕小,48px 的 section 内边距在 375px 宽的屏幕上占据了 12.8% 的宽度——过多。缩小到 32px 释放了宝贵的屏幕空间。
- 组件级间距微调或不调:组件内部的间距(如卡片内边距 16px)在移动端仍然需要——缩小到 12px 会让卡片内容"拥挤"。组件级间距的感知精度要求高,4px 的差异在组件内可感知——所以移动端不应该大幅调整组件级间距。
--space-6 从 24px 缩小到 20px:这是唯一的组件级间距调整——因为 24px 在移动端的卡片内边距中偏大(卡片宽度可能只有 343px,24px 的水平内边距占据 14%),缩小到 20px 释放了一点空间,同时保持了"宽松"的语义。
6.2 移动端的内间距方向调整
移动端的内间距需要特殊处理——水平方向大幅缩小,垂直方向保持或增大:
@media (max-width: 640px) {
:root {
--inset-lg: 12px 16px; /* 16px 24px → 12px 16px */
--inset-xl: 16px 20px; /* 24px 32px → 16px 20px */
--inset-hero: 32px 24px; /* 48px 40px → 32px 24px */
}
}
水平缩小、垂直保持的逻辑:
- 水平方向:移动端屏幕宽度有限,水平内边距必须缩小——否则卡片内容区域太窄,文字被迫频繁换行
- 垂直方向:移动端的垂直空间相对宽裕(用户可以滚动),垂直内边距保持或微调——确保卡片的"呼吸感"不因屏幕变窄而丧失
--inset-hero 的特殊反转:在桌面端是 48px 40px(垂直 > 水平),在移动端变为 32px 24px(垂直仍然 > 水平)——Hero/CTA 的垂直呼吸感在移动端同样重要,甚至更重要(移动端用户更容易感到"拥挤")。
6.3 移动端的外间距调整
@media (max-width: 640px) {
:root {
--stack-xl: 24px; /* 32 → 24 */
--stack-2xl: 32px; /* 48 → 32 */
}
}
section 间距从 48px 缩小到 32px:移动端的 section 间距需要缩小——因为移动端用户通过滚动来浏览页面,过大的 section 间距会让用户"滚动很久才看到下一个 section",产生"这个页面很空"的感觉。32px 的 section 间距在移动端仍然足以区分不同 section(配合背景色交替),同时不会让页面过长。
--stack-md(16px)和 --stack-sm(8px)保持不变:组件内部的元素间距在移动端不需要调整——这些间距定义的是"元素之间的关系亲密度",与屏幕尺寸无关。标题和副标题之间的关系在桌面端和移动端是相同的——8px 的间距在两个尺寸下都表达"密切相关"。
七、间距系统的工程原则
7.1 间距 Token 的命名规范
/* ✅ 角色命名 — 表达"这是什么空间关系" */
--inset-md、--stack-lg、--gap-sm
/* ✅ 尺度命名 — 表达"这是多大" */
--space-4、--space-12
/* ❌ 数值命名 — 表达"这是多少像素" */
--space-16px、--padding-24
双层命名体系:尺度层用尺寸命名(--space-4),语义层用角色命名(--inset-md)。组件代码只引用语义层 Token——当尺度调整时(如移动端缩放),语义层 Token 的映射自动更新,组件代码零改动。
7.2 间距的"不可见"验证
间距系统的验证比排版和色彩更难——因为间距是"不可见"的。用户看不到间距本身,他们只看到间距的效果:元素是否拥挤、关系是否清晰、节奏是否舒适。
验证方法一:间距剥离测试。临时给所有元素加上 outline: 1px solid red,让元素的边界可见。然后观察:
- 元素之间的红色边界是否有"重叠"或"紧贴"——如果有,说明间距太小
- 元素之间的红色边界是否有"大面积空白"——如果有,说明间距太大
- 同一层级的元素之间的间距是否一致——如果不一致,说明间距 Token 没有被正确使用
验证方法二:间距灰度测试。将页面截图转为灰度,观察元素的"分布密度":
- 如果某个区域元素密集(暗色块聚集)——间距太小,信息密度过高
- 如果某个区域元素稀疏(暗色块分散)——间距太大,信息密度过低
- 理想的灰度分布应该与第六篇的"注意力波浪"一致——Hero(中等密度)→ Dashboard(高密度)→ Metrics(中等密度)→ CTA(低密度)
验证方法三:间距一致性审查。用 DevTools 的 “Computed” 面板检查所有元素的 padding 和 margin——所有值都应该在间距尺度中找到对应。如果出现了 padding: 18px 这种不在尺度中的值,说明有组件脱离了间距系统。
7.3 间距的累积效应
与微交互(第五篇)类似,间距的效果也是累积的——单个间距值的选择不会产生明显的视觉差异,但所有间距值的协调一致会产生"这个界面很整齐"的整体感知。
"整齐"的感知来源:当所有间距值都是 4px 的倍数时,界面上所有元素的边界都对齐到一个隐含的 4px 网格上。用户看不到这个网格——但他们能感知到"所有元素的位置是有规则的"。这种"有规则"的感知就是"整齐"——而"整齐"的工程实现就是间距系统。
反面案例:如果卡片 A 的内边距是 16px,卡片 B 的内边距是 18px——2px 的差异用户"看不到",但他们能"感觉到"两张卡片"不一样"。这种"说不清哪里不对"的不适感,就是间距不一致的累积效应。
7.4 间距系统的降级
间距系统不需要 prefers-reduced-motion 那样的降级——间距是静态值,不涉及动画。但间距系统需要低性能设备的降级:
在低端设备上,大面积的留白(如 Hero 的 64px 内边距)不会影响性能——但大面积的阴影 + 留白组合会增加渲染负担。间距系统本身不需要降级,但间距与阴影的组合需要考虑低端设备的渲染能力。
实践建议:间距系统保持不变——它是布局的基础结构,不应该因设备性能而改变。如果低端设备需要降级,降级的是阴影和动效(第三篇和第五篇已讨论),而非间距。
八、总结:间距系统的五个层次
| 层次 | 做什么 | 核心原则 | 实践 |
|---|---|---|---|
| 1. 尺度层 | 按数学规则生成间距值 | 等差数列,4px 步长,分段加密 | 0-24px 用 4px 步长,24-96px 用 8px 步长 |
| 2. 语义层 | 建立间距与空间角色的映射 | 内间距/外间距/间隙三类语义 | inset / stack / gap 三组 Token |
| 3. 组件层 | 定义组件的间距消费纪律 | 组件禁触原子间距,只引用语义 Token | 卡片用 inset-lg,按钮用 inset-md |
| 4. 关系层 | 通过间距编码元素关系 | 间距越大关系越疏远(格式塔接近性) | stack-xs(4px) → stack-2xl(48px) |
| 5. 响应式层 | 定义间距在不同屏幕下的调整 | 页面级大幅缩,组件级微调或不调 | section 间距 48→32,卡片内边距保持 |
核心洞察:间距系统的本质是把"留白"从审美行为变成结构行为。尺度数学让间距值之间有可推导的关系;语义层让间距与空间角色绑定而非与具体数值锁死;组件层让间距的消费有纪律约束;关系层让间距成为元素关系的编码语言;响应式层让间距在不同屏幕下保持语义一致。
当这五层叠加在一起,间距就像排版(第八篇)和色彩(第九篇)一样获得了"工程结果"的性质:用户不会说"这个间距留得好",他们说"这个界面看着整齐、不挤"。整齐,是尺度纪律在运作;不挤,是语义分层在生效。间距系统的终极目标与排版和色彩同构——不是让界面"好看",而是让信息"好读"。好看是审美判断,好读是工程结果。
本文是设计系统系列的第十篇。系列文章:1. 从 Token 到主题 · 2. 从装饰到功能 · 3. 动效系统工程 · 4. 设计语言迁移实践 · 5. 组件级微交互设计 · 6. 页面叙事节奏 · 7. 数据可视化的组件级实践 · 8. 排版系统工程 · 9. 色彩系统工程 · 10. 间距系统工程
- 点赞
- 收藏
- 关注作者
评论(0)