布局
本技能决定元素放置的位置、彼此相隔多远以及它们的大小。它以相同的顺序做出相同的决策,并根据渲染后的像素检查结果。
在做出任何决策之前,请应用 product-ui §1(品味日志和优先级)和 §5(哪项技能拥有什么)。
按此顺序决策
- 外壳。 命名屏幕区域:侧边栏、工具栏、内容区、检查器。每个区域要么是一个滚动容器,要么不是。详细信息和宽度:shell-and-responsive.md。原因:外壳决定了内部布局必须适应的所有宽度。
- 宽度和度量。 根据内容设置内容列:表单440px,阅读720px且正文限制为
type度量(66ch),数据1200px。列永远不会因为屏幕宽而拉伸。原因:超过75个字符的行会失去读者,拉伸的表单会让眼睛在屏幕上扫来扫去。 - 间距。 仅使用下面的尺度,不使用其他。先用间距分组;在间距无法承载时,再添加填充,然后是分割线。spacing-and-density.md。
- 对齐。 选择共享的边缘,将所有内容对齐到其上。1-4px的偏差是一个错误。在视觉看起来不对的地方进行光学校正。spacing-and-density.md。
- 尺寸。 选择该区域的密度,然后是点击区域的大小。spacing-and-density.md。
- 最坏情况数据。 在完成前,根据最长、最短、缺失、翻译后和仍在加载的内容调整盒子大小。stability-and-text.md。
- 自适应。 使结构响应其容器,而不是设备。shell-and-responsive.md。
- 测量。 以2倍率渲染,测量间隙和边缘,修复像素显示的问题。measure.md。
尺度
一个尺度,4px基准。永远不要写一个不是步进值的间距值。原因:一次性值(10px, 14px, 18px)使节奏无法察觉和保持。
| Token | px | 典型用途 |
|---|---|---|
--space-xs | 4 | 标签到其描述;图标到标签(在chip组件中) |
--space-sm | 8 | 组内项目;控件内边距(块方向) |
--space-md | 12 | 带边框的控件之间;控件内边距(内联方向) |
--space-lg | 16 | 字段之间;紧凑卡片边距 |
--space-20 | 20 | 卡片内的块之间 |
--space-xl | 24 | 组之间;舒适卡片边距 |
--space-2xl | 32 | 区域之间 |
--space-3xl | 48 | 页面第一个标题上方 |
--space-4xl | 64 | 页面最后一个块下方 |
用于光学对齐的1-2px微调是唯一不在该尺度上的值。命名它们(--play-nudge: 2px)。
无需参考文档的规则
- 用间距分组。 组之间的间隔至少是组内间隙的2倍:内部8px意味着外部16px或更大。处处间距相等会导致分组模糊,而模糊在这里是一个功能性错误。
- 在添加分割线之前移除它们。 首先是间距,其次是填充,最后才是分割线,并且仅用于密集数据。原因:每一条线都会消耗注意力,但间距免费提供了相同的信息。
- 两样东西之间的间距上方等于下方。 原因:更靠近某个邻居的东西会被读作属于它。
- 控件必须看起来像控件。 按钮、链接或开关具有形状、下划线或工具栏位置。静态文本从不共享这些特征。原因:样式像文本的控件会隐藏,而样式像控件的文本会招来无效点击。
- 操作位于其操作的数据之上,绝不位于下方。设置某物的控件应紧邻它。
- 主要内容优先并且位于起始侧。 视觉顺序等于DOM顺序:永远不要使用
order、row-reverse或grid placement来重新排序内容。原因:键盘和屏幕阅读器的顺序遵循DOM。 - 对于需要镜像的场景,使用逻辑属性(
margin-inline-start、padding-block、inset-inline-end、text-align: start)。原因:无需第二张样式表即可支持RTL。 - 绝不要将关键控件放在窗口或侧边栏的底部边缘。 原因:人们会部分地将窗口推出屏幕。
- 滚动显示被隐藏的内容。 超出边缘的内容要有可见的提示:16-32px的露头或渐变淡出。
- 没有任何东西会移动。 悬停、选择和聚焦永远不能改变元素的大小。已加载的数据占据骨架所占据的盒子。
正反例对照
| 错误做法 | 正确做法 | 原因 |
|---|---|---|
表单内 gap: 12px,其字段集之间也是 gap: 12px | 内部 gap: 8px,之间 gap: 24px | 组需要至少2倍的间隔 |
padding: 10px 14px | padding: var(--space-sm) var(--space-md) | 仅使用步进值 |
| 标题在 x=24,描述在 x=26 | 两者都在 x=24 | 1-4px的偏差看起来像错误 |
图标按钮 16×16 且 padding: 0 | 16px 图标,::before { inset: -12px } 以获得 40px 目标 | 鼠标目标应为 40px |
.row + .row { margin-top: 8px } 且行上有悬停和点击 | .row { padding-block: 4px },无 margin | 行之间的间隙是死区,并使悬停闪烁 |
.menu { width: 200px } | width: max-content; min-width: 160px; max-width: 320px | 菜单贴合其最长标签 |
卡片上使用 @media (max-width: 768px) | 卡片父元素上使用 @container (width < 400px) | 窄侧边栏中的卡片需要自己的宽度,而不是屏幕的宽度 |
<img src> 没有尺寸,然后卡片跳动 | 设置 width、height 或 aspect-ratio | 盒子在字节加载之前就已存在 |
| 粘性标题,锚点落在它下面 | 滚动容器上设置 scroll-padding-block-start: 56px | 焦点和锚点绝不会落在chrome下面 |
完成前
在diff中搜索这些模式,然后验证。
| 模式 | 修复 |
|---|---|
margin、padding 或 gap 的值不在尺度中 | 最近步进值 |
组内和组间使用了同一个 gap 值 | 组内 ≤ 组间一半 |
仅图标按钮小于24px,或小于40px且无 ::before | 使用伪元素扩展 |
可点击行或菜单项之间的 gap 或 margin | 项内的内边距 |
菜单、按钮或文本框上的固定 width | max-content、min-width、max-width;文本使用 min-height |
可复用组件内的 @media (max-width | 父元素上的 @container |
外壳使用 height: 100vh | 100dvh |
持有长文本的 1fr 轨道或flex子项 | minmax(0, 1fr) 或 min-width: 0 |
镜像UI中的 margin-left、padding-right、left: 0、text-align: left | 逻辑形式 |
未设置 scroll-padding-block 的粘性页眉或页脚 | 设置为其chrome的高度 |
| 骨架行高或列宽与加载后的不同 | 匹配它们 |
无尺寸或 aspect-ratio 的 <img> | 添加一个 |
使用 measure.md 验证:以2倍率分别在390px宽度和外壳最宽宽度下渲染,运行脚本,裁剪每个候选项并计算像素。然后在存有粘性chrome的屏幕上Tab浏览,并加载最长的字符串。报告你无法检查的内容为“未验证”。