使用 :has() 解决长篇文本的垂直间距

Avatar of Liam Johnston
Liam Johnston

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元免费积分!

如果您曾经在包含大量 长篇文本 的网站上工作过(尤其是人们可以在 WYSIWYG 编辑器中输入大量文本的 CMS 网站),您可能不得不编写 CSS 来管理不同排版元素之间的垂直间距,例如标题、段落、列表等等。

令人惊讶的是,要正确设置这一点非常困难。 这也是 Tailwind Typography 插件和 Stack Overflow 的 Prose 这样的工具存在的其中一个原因——尽管它们处理的不仅仅是垂直间距。

在撰写本文时,Firefox 在 about:config 中的 layout.css.has-selector.enabled 标志 后面支持 :has()

为什么排版垂直间距很复杂?

当然,它应该像简单地声明每个元素(ph2ul 等)具有某个数量的顶部和/或底部边距一样简单……对吧? 遗憾的是,事实并非如此。 考虑以下所需行为

  • 长篇文本块中的第一个和最后一个元素不应在其上方或下方具有任何额外的空间(分别)。 这样,其他非排版元素仍然可以围绕长篇内容以可预测的方式放置。
  • 长篇内容中的各节应该在它们之间有一个很好的大空间。“节”指的是标题和所有属于该标题的后续内容。 在实践中,这意味着在标题之前有一个很好的大空间……但是如果该标题紧接在另一个标题之后,则
Example of a Heading 3 following a paragraph and another following a Heading 2.
当标题 3 紧跟在排版元素(如段落)之后时,我们希望在标题 3 上方有更多空间,但是当它紧跟在另一个标题之后时,我们希望有更少的空间。

您只需在 CSS-Tricks 上查看,即可了解这在哪些地方派上用场。 以下是从另一篇文章中提取的一些关于间距的屏幕截图。

A Heading 2 element directly above a Heading 3.
标题 2 和标题 3 之间的垂直间距
A Heading 3 element directly following a paragraph element.
标题 3 和段落之间的垂直空间

传统解决方案

我见过的典型解决方案包括将所有长篇内容放在一个包装 div(或语义标签,如果适用)中。 我的首选类名一直是 .rich-text,我认为我把它用作旧版 Wagtail CMS 的遗留,它会在渲染 WYSIWYG 内容时自动添加此类名。 Tailwind Typography 使用 .prose(以及一些修饰符类)。

然后,我们添加 CSS 来选择该包装器中的所有排版元素并添加垂直边距。 当然,要注意上面提到的关于堆叠标题和第一个/最后一个元素的特殊行为。

传统解决方案听起来很合理……问题是什么? 我认为有几个……

僵化的结构

必须在所有正确的位置添加像 .rich-text 这样的包装类,这意味着将特定的结构烘焙到您的 HTML 代码中。 有时这很有必要,但这感觉在这种情况下面不必要。 而且,很容易忘记在所有需要的地方这样做,尤其是在您需要将它用于 CMS 和硬编码内容的混合时。

当您想要能够分别从第一个和最后一个元素中修剪掉顶部和底部边距时,HTML 结构变得更加僵化,因为它们需要是包装元素的直接子元素,例如 .rich-text > *:first-child。 该 > 很重要——毕竟,我们不希望意外地使用此选择器选择每个 ulol 中的第一个列表项。

混合边距属性

:has() 出现之前,我们没有办法根据元素后面的内容来选择它。 因此,对排版元素进行间距的传统方法涉及使用 margin-topmargin-bottom 的混合。

  1. 我们首先使用 margin-bottom 设置元素的默认间距。
  2. 接下来,我们使用 margin-top 来间隔我们的“节”——即每个标题上方的很大空间。
  3. 然后,我们使用相邻兄弟选择器(例如 h2 + h3)覆盖那些很大的 margin-top,以确保当一个标题紧跟在另一个标题之后时,它们之间不会有很大空间。

现在,我不知道您如何看待,但我一直觉得在间隔事物时最好使用一个边距方向,通常选择 margin-bottom(假设 CSS gap 属性不可行,在这种情况下不可行)。 无论这是否是一件大事,或者是否属实,我都会让您自己决定。 但就我个人而言,我更愿意为间隔长篇内容设置 margin-bottom

折叠边距

由于 折叠边距 的存在,这种顶部和底部边距的混合本身并不是什么大问题。 只有两个堆叠边距中较大的一个将生效,而不是两个边距的总和。 但是……好吧……我不太喜欢折叠边距。

折叠边距是需要注意的又一件事情。 对于那些没有熟悉这个 CSS 奇特性质的初级开发者来说,这可能会造成混淆。 如果您将包装器更改为带有 flex-direction: columnflex 布局(例如,这在您单方向设置垂直边距时是不会发生的),间距将完全改变(即停止折叠)。

我或多或少知道折叠边距是如何工作的,并且我知道它们的设计目的。 我也知道它们在某些时候使我的生活更轻松。 但是它们在其他时候也使我的生活变得更困难。 我只是觉得它们有点奇怪,并且通常我宁愿避免依赖它们。

:has() 解决方案

以下是我尝试使用 :has() 解决这些问题的尝试。

总结一下它旨在进行的改进

  • 不需要包装类。
  • 我们正在使用 一致的边距方向
  • 避免折叠边距(这可能是改进,也可能不是,取决于您的立场)。
  • 不需要先设置样式,然后再立即覆盖它们。

关于 :has() 解决方案的说明和注意事项

  • 始终检查浏览器支持。 在撰写本文时,Firefox 仅在实验性标志后面支持 :has()
  • 我的解决方案不包含所有可能的排版元素。 例如,我的演示中没有 <blockquote>。 但是,选择器列表很容易扩展。
  • 我的解决方案也不处理您的特定长篇文本块中可能存在的非排版元素,例如 <img>。 这是因为对于我工作的网站,我们倾向于尽可能地锁定 WYSIWYG,使其仅包含核心文本节点,例如标题、段落和列表。 任何其他内容(例如引用、图片、表格等)都是单独的 CMS 组件块,这些块本身在页面上呈现时彼此间隔开。 但是,选择器列表可以扩展。
  • 出于完整性考虑,我只包含了 h1 我通常不允许 CMS 用户通过 WYSIWYG 添加 h1,因为页面标题会烘焙到页面模板中的某个位置,而不是在 CMS 页面编辑器中输入。
  • 我没有处理紧跟在同一级别标题之后的标题(h2 + h2)。 这意味着第一个标题不会“拥有”任何内容,这似乎是标题的错误使用方式(并且,如果我理解错了,请纠正我,但这可能违反了 WCAG 1.3.1 信息和关系)。 我也没有处理跳过的标题级别,这是无效的。
  • 我绝不贬低我提到的现有方法。 如果我再构建另一个 Tailwind 网站,毫无疑问我会使用出色的 Typography 插件!
  • 我不是设计师。 这些间距值是我凭感觉估算出来的。你可能(也应该)使用更好的值。

特异性和项目结构

我本来打算写一大堆关于传统方法和新的 :has() 方法如何适应 ITCSS 方法… 但是现在我们有了 :where()(零特异性选择器),你几乎可以为任何选择器选择你喜欢的特异性级别了。

也就是说,我们不再使用包装器 - .prose.rich-text 等 - 对我来说,感觉这应该属于“元素”层,也就是在你开始处理类级别特异性之前。我在我的示例中使用了 :where() 来保持特异性的一致。我两个示例中的所有选择器都有 特异性评分0,0,1(除了基本的重置)。

总结

所以这就是它,一个解决非常无聊问题的尖端解决方案!这种新方法仍然不能称为“简单”的 CSS - 正如我在开头所说,它比最初看起来更复杂。但除了有一些稍微复杂的选择器之外,我认为新方法总体上更有意义,而且不太严格的 HTML 结构似乎非常吸引人。

如果你最终使用了它,或者类似的东西,我很想知道它对你来说效果如何。如果你能想到改进它的方法,我也很乐意听到你的想法!