如果您曾经在包含大量 长篇文本 的网站上工作过(尤其是人们可以在 WYSIWYG 编辑器中输入大量文本的 CMS 网站),您可能不得不编写 CSS 来管理不同排版元素之间的垂直间距,例如标题、段落、列表等等。
令人惊讶的是,要正确设置这一点非常困难。 这也是 Tailwind Typography 插件和 Stack Overflow 的 Prose 这样的工具存在的其中一个原因——尽管它们处理的不仅仅是垂直间距。
在撰写本文时,Firefox 在 about:config
中的 layout.css.has-selector.enabled
标志 后面支持 :has()
。
为什么排版垂直间距很复杂?
当然,它应该像简单地声明每个元素(p
、h2
、ul
等)具有某个数量的顶部和/或底部边距一样简单……对吧? 遗憾的是,事实并非如此。 考虑以下所需行为
- 长篇文本块中的第一个和最后一个元素不应在其上方或下方具有任何额外的空间(分别)。 这样,其他非排版元素仍然可以围绕长篇内容以可预测的方式放置。
- 长篇内容中的各节应该在它们之间有一个很好的大空间。“节”指的是标题和所有属于该标题的后续内容。 在实践中,这意味着在标题之前有一个很好的大空间……但是如果该标题紧接在另一个标题之后,则不!

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


传统解决方案
我见过的典型解决方案包括将所有长篇内容放在一个包装 div
(或语义标签,如果适用)中。 我的首选类名一直是 .rich-text
,我认为我把它用作旧版 Wagtail CMS 的遗留,它会在渲染 WYSIWYG 内容时自动添加此类名。 Tailwind Typography 使用 .prose
类(以及一些修饰符类)。
然后,我们添加 CSS 来选择该包装器中的所有排版元素并添加垂直边距。 当然,要注意上面提到的关于堆叠标题和第一个/最后一个元素的特殊行为。
传统解决方案听起来很合理……问题是什么? 我认为有几个……
僵化的结构
必须在所有正确的位置添加像 .rich-text
这样的包装类,这意味着将特定的结构烘焙到您的 HTML 代码中。 有时这很有必要,但这感觉在这种情况下面不必要。 而且,很容易忘记在所有需要的地方这样做,尤其是在您需要将它用于 CMS 和硬编码内容的混合时。
当您想要能够分别从第一个和最后一个元素中修剪掉顶部和底部边距时,HTML 结构变得更加僵化,因为它们需要是包装元素的直接子元素,例如 .rich-text > *:first-child
。 该 >
很重要——毕竟,我们不希望意外地使用此选择器选择每个 ul
或 ol
中的第一个列表项。
混合边距属性
在 :has()
出现之前,我们没有办法根据元素后面的内容来选择它。 因此,对排版元素进行间距的传统方法涉及使用 margin-top
和 margin-bottom
的混合。
- 我们首先使用
margin-bottom
设置元素的默认间距。 - 接下来,我们使用
margin-top
来间隔我们的“节”——即每个标题上方的很大空间。 - 然后,我们使用相邻兄弟选择器(例如
h2 + h3
)覆盖那些很大的margin-top
,以确保当一个标题紧跟在另一个标题之后时,它们之间不会有很大空间。
现在,我不知道您如何看待,但我一直觉得在间隔事物时最好使用一个边距方向,通常选择 margin-bottom
(假设 CSS gap
属性不可行,在这种情况下不可行)。 无论这是否是一件大事,或者是否属实,我都会让您自己决定。 但就我个人而言,我更愿意为间隔长篇内容设置 margin-bottom
。
折叠边距
由于 折叠边距 的存在,这种顶部和底部边距的混合本身并不是什么大问题。 只有两个堆叠边距中较大的一个将生效,而不是两个边距的总和。 但是……好吧……我不太喜欢折叠边距。
折叠边距是需要注意的又一件事情。 对于那些没有熟悉这个 CSS 奇特性质的初级开发者来说,这可能会造成混淆。 如果您将包装器更改为带有 flex-direction: column
的 flex
布局(例如,这在您单方向设置垂直边距时是不会发生的),间距将完全改变(即停止折叠)。
我或多或少知道折叠边距是如何工作的,并且我知道它们的设计目的。 我也知道它们在某些时候使我的生活更轻松。 但是它们在其他时候也使我的生活变得更困难。 我只是觉得它们有点奇怪,并且通常我宁愿避免依赖它们。
: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 结构似乎非常吸引人。
如果你最终使用了它,或者类似的东西,我很想知道它对你来说效果如何。如果你能想到改进它的方法,我也很乐意听到你的想法!
你分享了如此有见地的内容,继续努力吧,感谢你与我们分享这些宝贵的东西。
非常感谢你 :) 我会努力的!
使用 Firefox 的同事指出,即使启用了
:has()
支持标志,:has(* +)
也不起作用。看起来可能是这里的一个 bug:https://bugzilla.mozilla.org/show_bug.cgi?id=1774588
希望 Firefox 开发人员在正式发布之前修复它。鉴于许多更酷的
:has()
演示依赖于这些类型的选择器,我相信他们会修复它 :)没错。因此,为了在你的代码中检测功能支持,你必须使用
:has(+ *)
而不是:has(*)
。有关详细信息,请参见 https://brm.us/feature-detect-has。
很棒的文章。 期待所有浏览器都支持
:where
、:is
和:has
。我已经迫不及待了这是一个仅由使用
margin-bottom
而不是margin-top
引起的问题的解决方案,而这样做的理由是“但我个人更愿意为长篇内容设置margin-bottom
”。“列表项间距”示例是最明显的案例:切换到margin-top
,你只需要一个简单的li + li
就可以了。:has
将在许多场景中非常有用。这不是其中之一,除非你故意决定反转边距的方向。让我们不要没有充分的理由就让用户落后。我在其他地方写了一个详细的回复。
感谢 Shiv 的详细回复。这可能是我第一次有人写一篇关于我所说的话的博文。你提出了一些很好的观点,我将在这里尝试解决。
使用 margin top。
我过去曾使用过 lobotomised owl 选择器。但是我从未发现它在全站范围内有用(如原始帖子所述:https://alistapart.com/article/axiomatic-css-and-lobotomized-owls/),因为我被交到手的那些设计类型不适合大多数元素的默认垂直间距。因此,当我使用它时,它总是被作用域限定的。事实上,其中一个案例就是
.rich-text
作用域,即长篇文本间距。我认为这是一个完全可以的解决方案(虽然它并非没有缺点 - 请参见“严格结构”)。浏览器支持。
你指出
:has()
的支持度不高,我的代码不能优雅地降级。对此我无话可说。我只想说我在文章中提到了浏览器支持 - 两次 -。与所有:has()
演示一样,它不应该在生产环境中使用。选择器复杂性。
我同意我的解决方案很复杂(额外的特异性处理并没有帮助)。我试图通过代码注释使其不那么复杂 - 是否成功,我不知道。
但我个人并不认为这个块不那么复杂
但这可能只是我个人观点。
我注意到你没有解决“严格结构”部分(即使用
.rich-text
作用域)。我觉得这是我建议方法最吸引人的好处。至少对于我参与的大多数网站来说是如此。对于可能混合使用 CMS 和硬编码内容的内容,必须确保包装器是所有散文元素的直接父级(以消除额外的顶部/底部空间)可能很麻烦。以你的博客为例,它似乎非常适合 lobotomised owl。即扁平的 DOM,单列,以文本为主。但对于我参与的大多数网站来说并非如此。
lobotomised owl 是一种解决方案,而且通常是一种很好的解决方案。我的文章旨在作为我今天通常看到的解决方案的替代方案。此外,我认为一旦
:has()
成为主流(并得到良好的支持),开发人员对它更加熟悉之后,我们将会看到它更多地用于像长篇文本这样的案例中,因为我认为它能让你获得更细粒度的控制。我必须同意 Shiv 的观点。我一直都很好地管理着,没有使用 Andy Bell 的指南就没有使用
:has()
。我不记得它是在哪里描述的,但我实现如下(可以扩展到 h1、h4 和其他元素)这篇文章是一个关于如何使用
:has()
的有趣的演示,是一个很好的学习资源,但我们可以保持简单感谢你的回复,Christopher。“…一个关于如何使用
:has()
的有趣的演示 […] 一个很好的学习资源” 正是我想要达成的效果 :D我也很欣赏 Andy Bell 在这方面的工作,特别是视口大小/空间缩放方面。
我怀疑我的方法是否会(或应该)更广泛地推广开来。就目前而言,它非常具有实验性,因为 a)
:has()
还没有得到完全支持,b) 我还没有实际使用过它(除了 Codepen)。我认为在某些情况下,不需要包装器(在你的案例中为
.flow
)可能非常有吸引力。但它的实际有用性肯定会因项目而异。至于简单性,在我同意之前,我可能想比较两种方法的完整示例。即:两种方法都考虑了所有/大多数“流”元素,两种方法都在块的开始/结束处削减了顶部和底部空间,两种方法都在有和没有堆叠标题的情况下处理标题间距,这些标题因标题级别而异。你可能是对的。