surface-separation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Surface Separation

Surface 分离

A surface — a card, a panel, a toolbar, a row — needs to read as distinct from whatever sits behind it. There are exactly three ways to achieve that, and the mistake is almost always using more than one at full strength rather than choosing badly between them.
一个 surface——卡片、面板、工具栏、列表行——需要让人一眼看出它与后方内容不同。实现这一点恰好有三种方式;常见错误几乎总是把不止一种方式同时用到最大强度,而不是在它们之间做出正确选择。

Pick One Strategy Per Surface

每个 Surface 选择一种策略

StrategyMechanismReads asBest for
Border1px line at the boundaryPrecise, drawn, documentaryDense UI, tables, forms, anything where the edge itself carries meaning
ElevationShadow implying heightFloating, temporary, aboveModals, dropdowns, popovers, drag states
FillContrasting background toneGrouped, embedded, part of the pageSections, sidebars, alternating rows, inline callouts
Each answers a different question. A border says where the surface ends. An elevation says how far above the page it sits. A fill says what it belongs to. Choose based on which question the user actually has.
The rule: never combine all three on the same surface. A card that carries a visible border, a drop shadow, and a background tone distinct from the page is stating its boundary three times. Nothing is emphasised because everything is. This is the most common single cause of a UI that someone describes as "busy" or "heavy" without being able to point at why.
策略机制传达的视觉感受最适合
Border边界处使用 1px 线条精确、清晰、有文档感密集 UI、表格、表单,以及任何边界本身具有意义的内容
Elevation通过 shadow 暗示高度悬浮、临时、位于上层modals、dropdowns、popovers、拖拽状态
Fill使用有对比度的背景色调分组、嵌入、属于页面的一部分sections、sidebars、交替行、行内 callouts
它们回答的是不同的问题。border 说明 surface 在哪里结束;elevation 说明它比页面高出多少;fill 说明它归属于什么。根据用户真正需要回答的问题来选择。
**规则:**绝不要在同一个 surface 上同时使用全部三种方式。如果一个卡片同时具有明显的 border、drop shadow,以及与页面明显不同的背景色调,就等于三次强调它的边界。结果是什么都没有被强调,因为所有元素都被强调了。这是最常见的单一原因:人们会形容某个 UI “busy”或“heavy”,却说不出具体原因。

The Legitimate Pair

合法的组合

Border and elevation together are correct in one specific case: a light surface on a light page, where a subtle shadow alone washes out and the edge goes fuzzy. Here the border defines the boundary and the shadow supplies the depth — they do different jobs. See [[elevation-and-depth]] for the shadow scale and the border tone that pairs with it.
What keeps this to two signals is that the surface fill stays at or near the page tone. The moment you also push the background a clear step away, the edge is being stated a third time.
border 与 elevation 同时使用,只在一个特定场景中是正确的:浅色 surface 位于浅色页面上,单独的细微 shadow 会完全消失,边缘也变得模糊。此时 border 用来界定边界,shadow 用来提供深度——二者承担不同职责。参见 [[elevation-and-depth]],了解 shadow 层级以及与之搭配的 border 色调。
让这里只保留两种信号的关键,是 surface 的 fill 保持与页面色调一致或接近。一旦背景再明显偏离一个层级,边界就被表达了第三次。

Nesting

嵌套

A surface inside a surface should change strategy, not intensify the same one. A bordered card containing bordered rows produces a grid of lines with no hierarchy. Use a fill shift for the inner rows, or drop the inner borders and rely on spacing — see [[gestalt-ui-organisation]] for when proximity alone is enough to group.
Depth of nesting is the real constraint. Past two levels of visible separation, no strategy reads clearly. Flatten the structure instead.
嵌套在 surface 内的 surface 应改变策略,而不是强化同一种策略。一个带 border 的卡片内又包含带 border 的列表行,会形成没有层级的线条网格。对内部列表行改用 fill 色阶变化,或去掉内部 border、依靠间距完成分组;何时仅靠邻近性就足以形成分组,参见 [[gestalt-ui-organisation]]。
嵌套深度才是真正的限制。当可见的分离超过两级后,任何策略都无法清晰表达。此时应将结构扁平化。

Opacity Is a State, Not a Tint

Opacity 是状态,不是色调

Opacity communicates that an element is temporarily not fully available: disabled, loading, pending, dragging. That is its entire job in a UI.
css
.btn:disabled     { opacity: 0.4; }
.card.is-loading  { opacity: 0.7; }
.row.is-dragging  { opacity: 0.5; }
Reserving it for this makes it legible. When opacity appears for any other reason, the signal stops meaning anything.
opacity 用来传达某个元素暂时没有完全可用:disabled、loading、pending 或 dragging。在 UI 中,这就是它唯一的用途。
css
.btn:disabled     { opacity: 0.4; }
.card.is-loading  { opacity: 0.7; }
.row.is-dragging  { opacity: 0.5; }
把 opacity 保留给这些用途,才能让信号清晰可辨。一旦出于其他原因使用 opacity,该信号就失去意义。

Never Tint With Transparency

永远不要用透明度制作 Tint

Reaching for
rgba(brand, 0.1)
or
opacity: 0.6
to produce a lighter shade of a colour is the anti-pattern. Define the tint as a real token instead:
css
/* Wrong — the rendered colour depends on whatever is behind it */
.badge { background: rgba(37, 99, 235, 0.12); }

/* Right — a resolved token with a known value */
.badge { background: var(--color-brand-50); }
Three things break with the transparent version:
  1. The colour is not knowable. It resolves against whatever sits behind it, which changes with the parent, the theme, and the scroll position. Two instances of the same component render as different colours.
  2. Contrast cannot be verified. WCAG ratios are computed between two concrete colours. A translucent layer has no concrete colour until it composites, so no automated check can confirm the text on it is readable — and it will pass in one context and fail in another.
  3. The design system loses the value. A tint that exists only as an alpha multiplier is not a token. It cannot be referenced, exported, versioned, or swapped per theme.
Derive tints algorithmically from the brand colour and store them as tokens — [[algorithmic-color-palette]] covers deriving a full ramp from one or two source colours.
如果为了得到某种更浅的颜色而使用
rgba(brand, 0.1)
或
opacity: 0.6
,就是反模式。应该把 tint 定义为真实的 token:
css
/* Wrong — the rendered colour depends on whatever is behind it */
.badge { background: rgba(37, 99, 235, 0.12); }

/* Right — a resolved token with a known value */
.badge { background: var(--color-brand-50); }
透明版本会破坏三件事:
  1. 颜色无法确定。 它会根据后方内容进行合成,而后方内容会随父级、主题和滚动位置发生变化。同一个组件的两个实例可能呈现为不同颜色。
  2. 无法验证对比度。 WCAG 比率是在两个具体颜色之间计算的。半透明图层在合成前没有确定的颜色,因此任何自动化检查都无法确认其上的文字是否可读;它可能在一个上下文中通过,却在另一个上下文中失败。
  3. 设计系统无法沉淀该值。 仅以 alpha 乘数形式存在的 tint 不是 token。它无法被引用、导出、版本管理或按主题替换。
应通过算法从品牌色推导出各种 tint,并将其存储为 token;[[algorithmic-color-palette]] 介绍了如何从一到两个源颜色推导出完整的色阶。

Translucency on Translucency

透明叠加透明

The failure compounds when a translucent element sits on another translucent element. Each layer multiplies against the last, so the final rendered colour depends on the whole chain beneath it — and nobody knows that chain while authoring the component.
Never set
opacity
on an element whose
background-color
already carries an alpha channel.
If both appear on the same component, one of them is doing the wrong job: either the background should be an opaque token, or the opacity should be a state and the background should not be translucent.
Glass and frosted effects are the deliberate exception, because unpredictable background is the point. They come with a cost: the text on them needs its own opaque backing, or a contrast guarantee that holds against the worst-case backdrop. Do not let a glass surface carry body copy without one.
当一个半透明元素位于另一个半透明元素之上时,问题会进一步恶化。每一层都会与下一层相乘,因此最终呈现的颜色取决于其下方的整条图层链,而在编写组件时,没有人会知道这条链。
绝不要对
background-color
已经带 alpha 通道的元素设置
opacity
。
如果两者同时出现在同一个组件上,其中一个就承担了错误的职责:背景应该使用不透明的 token,或者 opacity 应该用于表示状态,而背景不应是半透明的。
玻璃和磨砂效果是刻意设置的例外,因为不可预测的背景正是它们的效果来源。但它们也有代价:其上的文字需要自己的不透明底色,或者需要一种在最差背景情况下仍然成立的对比度保证。不要在没有这两项保障的情况下,让玻璃 surface 承载正文内容。

Disabled Is the Exception That Proves It

Disabled 是证明这一点的例外

Disabled elements are exempt from WCAG contrast minimums, which is exactly why low opacity is correct there and nowhere else — it is the one place where deliberately unreadable is the intended message. Opacity alone is still not enough to communicate disabled: pair it with
cursor: not-allowed
and a non-interactive state. [[button-states]] has the full state matrix.
disabled 元素不受 WCAG 对比度最低要求的限制,这正是低 opacity 只应在那里使用、而不应出现在其他地方的原因:这是唯一一个刻意让人无法阅读反而能传达预期信息的地方。不过,仅靠 opacity 仍不足以表达 disabled 状态:应将它与
cursor: not-allowed
以及非交互状态搭配使用。完整的状态矩阵见 [[button-states]]。

Checklist

检查清单

  • Does each surface use one separation strategy, or a border/elevation pair with a page-tone fill?
  • Is any surface carrying a border, a shadow, and a distinct fill at once?
  • Does nested separation change strategy rather than repeat it, and stop at two levels?
  • Is every
    opacity
    value in the codebase attached to a state — disabled, loading, dragging?
  • Are all tints real tokens rather than alpha multipliers?
  • Does any component set
    opacity
    on top of an already-translucent background?
  • Does text on a glass surface have an opaque backing or a worst-case contrast guarantee?
  • 每个 surface 是否只使用一种分离策略,或采用 border/elevation 组合并让 fill 保持页面色调?
  • 是否有 surface 同时带有 border、shadow 和明显的 fill?
  • 嵌套分离是否改变策略而不是重复同一策略,并且不超过两级?
  • 代码库中的每个
    opacity
    值是否都对应某种状态——disabled、loading、dragging?
  • 所有 tint 是否都是真实 token,而不是 alpha 乘数?
  • 是否有组件在半透明背景上再次设置
    opacity
    ?
  • 玻璃 surface 上的文字是否具有不透明底色,或有最差情况下的对比度保证?