间距系统工程:从尺度数学到空间语义的层级构建

举报
霄均 发表于 2026/10/21 18:27:14 2026/10/21
【摘要】 间距系统工程:从尺度数学到空间语义的层级构建本文是设计系统系列的第十篇。前九篇分别讨论了 Token 架构、风格光谱、动效系统、设计语言迁移、组件级微交互、页面叙事节奏、数据可视化组件、排版系统和色彩系统——间距在其中每篇都以"已知条件"出现:--space-4、padding: 48px、gap: 16px……但间距自身的生成规则、语义分层和组件消费策略从未被独立拆解。本文补上这块拼图:...

间距系统工程:从尺度数学到空间语义的层级构建

本文是设计系统系列的第十篇。前九篇分别讨论了 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)——水平内间距大于垂直内间距。这是因为:

  1. 文字的水平延展性:中英文文字在水平方向上自然延展,水平内间距需要更大以容纳文字宽度的变化
  2. 垂直方向的紧凑预期:用户对垂直方向的紧凑度更敏感——按钮的垂直内间距过大会让按钮看起来"膨胀",而水平内间距过大只让按钮"变长"

--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 组件间距的三问

组件选间距时回答三个问题:

  1. 这是内间距、外间距还是间隙?(inset / stack / gap)
  2. 关系亲密度或空间层级是什么?(xs / sm / md / lg / xl)
  3. 是否需要响应式调整?(移动端是否缩放)

以"卡片"为例:

  • 卡片的 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. 间距系统工程

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。