通过显式设置 HTML 方向、使用逻辑属性控制方向性间距,并在布局项之间使用 gap,在从左至右和从右至左的页面之间共享一个 CSS 文件。本指南会构建一个本地双页面示例,供你在 Mac 浏览器中对比。

任务是让同一个小组件在行内方向变化时仍保持预期的间距。你需要一款纯文本编辑器和一款当前版本的桌面浏览器。示例使用虚构标签和装饰圆形,不需要框架、外部资源或 JavaScript。

共享样式表应控制什么?

由 HTML 负责方向,由样式表负责间距。两个页面加载同一个 CSS 资源,并使用相同的组件类;页面根元素提供 dir="ltr" 或 dir="rtl"。HTML 将 rel="stylesheet" 定义为一种外部资源链接,用于为文档提供样式。HTML 标准:样式表链接。

方向已知时,使用 HTML dir。后代元素通常会继承方向,而 lang 不会设置方向。在这个受控对比中,优先显式指定方向,而不是使用 dir="auto":自动方向取决于内容,会让拉丁字母示例标签选择从左至右。MDN:dir 属性。

关键在于区分物理边缘与内容流中的位置。在此示例的 horizontal-tb 书写模式下,行内起始方向在从左至右布局中表示左侧,在从右至左布局中表示右侧。块起始方向始终是顶部。逻辑属性可以直接表达这些关系。MDN:逻辑外边距、边框和内边距。

预期间距 共享 CSS 选项 方向变化后的结果
组件行首边缘内侧的间距 padding-inline-start 移至另一侧物理边缘
两个行内边缘内侧的间距 padding-inline 按新方向应用起始和结束值
Flexbox 布局项之间的间距 gap 保持指定的项间距
此水平布局中上方和下方的间距 padding-block 仍位于上方和下方
行内流中元素之前的外边距 margin-inline-start 映射到新的行内起始侧

外边距和内边距的映射遵循元素的书写模式和方向。gap 则会在支持的布局容器中创建间隔。这是两个不同的用途:用内边距围绕内容,用间距分隔相邻元素。MDN:逻辑间距,MDN:gap。

构建共用一个 CSS 文件的两个本地页面

在 Mac 上创建名为 spacing-demo 的文件夹。将以下文件保存为纯文本,并使用正确的扩展名。避免使用会在 .html 或 .css 后添加 .txt 的编辑器。

spacing-demo/
  ltr.html
  rtl.html
  components.css

将以下完整 HTML 文档保存为 ltr.html。然后另存一份为 rtl.html,只将根元素属性从 dir="ltr" 改为 dir="rtl"。保持样式表引用和组件标记相同。由于这是虚构的布局对比,标签仍使用英文。

<!doctype html>
<html lang="en" dir="ltr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Component spacing sample</title>
  <link rel="stylesheet" href="components.css">
</head>
<body>
  <main class="demo">
    <h1>Spacing sample</h1>
    <article class="sample-card">
      <span class="sample-card__shape" aria-hidden="true"></span>
      <div class="sample-card__copy">
        <h2 class="sample-card__title">Module A</h2>
        <p class="sample-card__description">A compact sample tile</p>
      </div>
      <span class="sample-card__tag">Set B</span>
    </article>
  </main>
</body>
</html>

接着,将以下完整样式表保存为 components.css。特意设置不相等的行内内边距,方便观察方向变化:较大的缩进应跟随行内起始方向。

:root {
  --space-small: 8px;
  --space-medium: 16px;
  --space-large: 24px;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  color: #243447;
  background: #f2f5f8;
}

.demo {
  writing-mode: horizontal-tb;
  max-width: 640px;
  margin-inline: auto;
}

.sample-card {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space-medium);
  padding-block: var(--space-medium);
  padding-inline: var(--space-large) var(--space-small);
  border: 1px solid #8594a5;
  border-radius: 12px;
  background: #ffffff;
}

.sample-card__shape {
  flex: 0 0 32px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: #5477a3;
}

.sample-card__copy {
  flex: 1 1 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

.sample-card__title,
.sample-card__description {
  margin: 0;
}

.sample-card__title {
  font-size: 18px;
}

.sample-card__description {
  margin-block-start: var(--space-small);
}

.sample-card__tag {
  flex: 0 0 auto;
  padding-block: 4px;
  padding-inline: var(--space-small);
  border: 1px solid #8594a5;
  border-radius: 6px;
}

这里,padding-inline: 24px 8px 表示起始内边距后接结束内边距,不表示左侧后接右侧。使用单个值时,该值会同时应用于两个行内边缘。MDN:padding-inline。

两个页面都保留 flex-direction: row。Flexbox 的行方向本来就遵循容器方向:从左至右时从左向右排列,从右至左时从右向左排列。在从右至左版本中添加 row-reverse 会再次反转方向。MDN:flex-direction。

中性圆形没有方向含义,因此其图形保持不变,位置则随行方向改变。拉丁字母标签也会保持原有的字母顺序;本示例检查的是组件布局,而非翻译后的文本。

如何确认间距确实由共享样式表控制?

在 Mac 浏览器中打开两个 HTML 文件,并在相同窗口宽度下进行比较。下表列出的是根据声明推算的预期值,并非浏览器测量结果。卡片边框与内边距是分开的。

检查项 从左至右页面 从右至左页面
圆形位置 左侧第一个元素 右侧第一个元素
卡片左侧内边距 24px 8px
卡片右侧内边距 8px 24px
相邻 Flexbox 布局项之间的间距 16px 16px
卡片上方和下方的内边距 16px 16px
描述文本之前的间距 上方 8px 上方 8px

单独测试资源共享是否生效,与方向测试分开进行。在 components.css 中将 --space-medium 从 16px 改为 20px,保存并重新加载两个页面。由于相关声明引用同一个变量,两个页面的项间距和块方向内边距都应增大。对比后将其恢复为 16px。

然后缩窄浏览器窗口,并将描述替换为较长的虚构标签,例如 Module A extended sample description。检查文本换行后是否遮挡圆形或标签。这还可以进行第二项测试:组件在某个宽度下可能镜像正确,在另一个宽度下却会显得拥挤。

不要只根据 gap 判断所有可见距离。元素外边距、内部内边距和分散对齐都可能使可见间隔大于声明的间隔。示例重置了文本外边距,并避免使用分散对齐,让对比更清晰。MDN:gap 说明。

如何迁移现有组件,同时避免间距冲突?

先理解每条现有声明的作用。文本行首的缩进通常对应行内起始方向;有意固定在某个物理边缘的间距仍可能需要使用物理属性。应根据设计意图进行转换,而不是机械地替换每个 left。

对于类似此示例的组件,请按以下顺序操作:

  1. 找出提供方向的祖先元素。
  2. 将方向性内部间距替换为逻辑内边距。
  3. 必要时,将方向性外部间距替换为逻辑外边距。
  4. 使用父元素的 gap 设置 Flexbox 布局项之间的常规间距,并移除先前用于提供相同间距的外边距。
  5. 在两个方向下重复测试不等内边距和窄窗口布局。

迁移时检查是否存在旧的物理属性声明和后续简写声明。映射到同一侧的逻辑属性和物理属性会一起参与层叠;逻辑属性不会仅仅因为它是逻辑属性就自动优先。在其他层叠条件相同时,后续的 padding 声明可能会重置此前通过逻辑属性设置的内边距。CSS 逻辑属性和值第 1 级:盒模型属性。

还要留意方向的边界。嵌套组件如果自身设有 dir="ltr",即使位于从右至左页面中,也应使用从左至右的映射。反过来,margin-inline 本身不会继承,因此应为每个组件设置所需的间距声明,而不要指望祖先元素的外边距转移到它上面。MDN:direction 的继承,MDN:margin-inline 形式化定义。

哪些浏览器限制会影响这种做法?

MDN 标注 padding-inline 和 margin-inline 自 2021 年 4 月起已广泛支持于各浏览器。这意味着当前桌面浏览器支持这些属性,但不保证所有旧版浏览器或这些参考页面提及的所有较新语法都支持它们。此示例使用常规长度值和 auto,不使用较新的尺寸函数。MDN:padding-inline,MDN:margin-inline。

Flexbox 间距的支持历程有所不同。Safari 14.1 加入了该功能;Safari 12 起已支持 Grid 间距。WebKit 特别指出,属性功能查询无法区分这两种上下文,因此仅使用 @supports (gap: 16px) 并不足以检测旧版 Safari 对 Flexbox 间距的支持情况。WebKit:Safari 14.1 中的 WebKit 新功能。

如果目标浏览器支持逻辑外边距,但不支持 Flexbox 间距,请在主样式表规则之后追加以下替代代码片段。它为这个特定的单行组件提供间距。请按示例所示,用它替代当前启用的间距规则;它并非适用于多行换行布局的通用方案。

/* Optional single-row replacement for Flexbox gap. */
.sample-card {
  gap: 0;
}

.sample-card > * + * {
  margin-inline-start: var(--space-medium);
}

如果浏览器也不支持逻辑属性,此回退方案就不够用。可使用当前版本的浏览器完成本地任务,或针对旧版目标浏览器编写并测试明确的从左至右和从右至左物理属性规则。将此浏览器特定的选择与共享样式表的主要设计分开处理。

完成后的对比应体现一个一致的约定:HTML 提供方向,共享 CSS 提供间距,组件的行首缩进跟随行内起始方向。每当更改组件的间距或布局时,都保留这两个本地页面,作为精简的回归测试样例。

来源