你对 CSS 布局了解多少?

Avatar of Brad Westfall
Brad Westfall

DigitalOcean 提供适合您旅程每个阶段的云产品。 立即开始使用 $200 免费信用额度!

CSS 良好的体验和漫长而令人沮丧的体验之间的区别,通常在于一些细微的差别。 CSS 的确很微妙。 我看到人们在布局方面经常遇到困难,这是最常见的问题之一。 我个人喜欢研究模式。 我注意到,我倾向于使用一小部分模式来解决大多数布局问题。 本文介绍了我用来解决布局挑战的 CSS 模式。 此外,无论使用哪种 CSS 方法,本文都探讨了如何采用无视方法,例如 SMACSS、BEM,甚至是 CSS-in-JS 的热门话题,因为它们都关注属性本身,而不是架构、组织或策略。

就当是娱乐,让我们先做个测试

我们将使用一个我创建的平台,名为 Questionable.io,我用它创建了一个测试,我们将在下面进行。 不用担心,不会收集任何个人数据,结果是匿名的,而且完全免费。

测试的目的是看看你是否能够在没有先学习材料的情况下,在上下文中识别出特定的 CSS 行为和问题。 我并不是故意要让测试变得困难,但 CSS 布局的细微差别往往比较复杂,尤其是没有太多接触的情况下。 请记住,这只是为了娱乐。 结果并不能说明你的能力,但希望你能够从中有所收获。

测试包含 10 道题,应该在 10 分钟或更短的时间内完成。

参加 CSS 布局测验

对测试感兴趣,但不想参加? 这里有一个链接,包含测试题及其正确答案。

已经做完了吗? 太棒了! 让我们逐题讲解,以便更好地理解测试中涵盖的布局模式。

问题 1:盒子模型

学习盒子模型应该是任何人的首要任务。 虽然 这篇 CSS-Tricks 盒子模型文章 可能有点旧了,但不要低估它对现代 CSS 的价值和相关性。 盒子模型是几乎所有与布局相关的 CSS 主题的先决条件。

这个问题正在测试如何获取盒子模型的 *计算* 宽度。 该盒子显然有 width: 100px;,但事实证明,盒子模型的默认规则width 属性应用于盒子的内容层。 计算宽度(在页面上渲染的宽度)是内容层、填充层和边框层的总和。 因此,答案是 112px

.box {
  width: 100px; /* Take this */
  height: 50px;
  padding: 5px; /* Plus this x2 for left and right */
  border: 1px solid red; /* Plus this x2 for left and right */
  background-color: red;
  /* = 112px of computed width */
}

如果你遇到过 UI 中最后一列或选项卡换行到下一行的情况,而你确信五个选项卡(都设置为 width: 20%;)加起来是 100%,那么很可能这就是问题所在。 5 个选项卡的宽度为 20% 的确加起来是 100%,但如果涉及填充和/或边框,它们会增加宽度,因此最后一列无法容纳在同一行内。

在引入 CSS3 的同时,CSS 中出现了一个名为 box-sizing 的新工具。 它允许我们更改要应用 width 的盒子模型的层。 例如,我们可以使用 box-sizing: border-box;,这意味着我们希望任何 width 规则都应用于边框层的外部,而不是内容层。 在这个问题中,如果应用了 box-sizing: border-box;,则计算宽度将为 100px。

这对你们中的一些人来说已经是老生常谈了,但对于专业人士和新手来说都是一个很好的提醒。

关于盒子模型以及如何使用 box-sizing 作为重置,以便一次性将它应用于整个项目,有很多文章。 盒子大小继承盒子大小可能是一个更好的最佳实践 是 CSS-Tricks 上的两个很好的文章,可以帮助你入门。

问题 2:边框很霸道

第二个测试问题几乎可以看作是第一个问题的“第二部分”。 请记住,阅读“盒子模型有多层,它们都对计算的宽度和高度有贡献”是一回事,能够在实际情况下识别盒子模型问题是另一回事。 这个问题对于那些做了一段时间 CSS 的人来说,算是一个经典问题。 它的根源在于边框占用空间,并且会推挤周围的东西,因为它们是盒子模型的一部分。 在状态转换(如 :hover)期间引入边框,将意味着盒子会变大,从而向下推挤后续的盒子。 它还会导致 抖动体验

查看 CodePen 上的示例 CSS-Tricks:边框是维度,作者是 Brad Westfall (@bradwestfall)。

在所有可能的解决方案中,在初始“未悬停”状态下使用 border: 2px solid transparent 是唯一能解决问题的方案。 box-sizing 无法解决这个问题,因为我们没有明确设置高度。 如果我们设置了高度,那么边框将被计算在高度的内部,并且不会发生偏移 - 但事实并非如此。

还有一些其他解决方案没有被列为可能的答案。 一种是使用 box-shadow 创建伪边框,另一种是使用 outline 而不是 border。 这两种方法都不会在状态更改期间导致偏移,因为它们不是盒子模型中的层。 这里还有另一篇 CSS-Tricks 文章 可以帮助你了解更多关于这些解决方案的信息

请记住,outline 不支持 border-radius

问题 3:绝对定位与固定定位

除了知道何时使用它们以及 它们在视觉行为上的差异 之外,了解每种定位方法如何使用其 toprightbottomleft 属性附加到父元素也非常重要。

首先,让我们回顾一下 包含块。 简而言之,包含块通常是任何给定元素的父元素。 但是,绝对元素和固定元素的包含块规则不同

1. **对于绝对元素:** 包含块是最近的祖先父元素,其 static 属性不为 static。 例如,当元素被绝对定位,并且包含 toprightbottomleft 属性时,它将相对于任何具有 absoluterelativefixedsticky 位置的父元素进行定位。
2. **对于固定元素:** 包含块是视窗,无论任何父元素具有除 static 以外的 position 值。 此外,滚动行为与 absolute 不同,position: fixed; 元素在视窗滚动时保持“固定”到视窗,因此得名。

许多开发人员认为绝对定位的元素只会寻找最近的 position: relative; 父元素。 这是一个常见的误解,因为 position: relative 通常与 position: absolute; 配合使用,以创建包含块。 它被普遍使用的原因是 relative 保持父元素 在流中,这通常是理想的行为。 但是,有时绝对定位元素的包含块也是绝对定位的。 根据具体情况,这完全可以接受。 如果所有父元素都是静态的,那么绝对定位的元素将附加到视窗 - 但它会随着视窗一起滚动

查看 CodePen 上的示例 CSS-Tricks:绝对定位滚动,作者是 Brad Westfall (@bradwestfall)。

以上两条规则还有一个鲜为人知的注意事项:只要父元素具有 transform 属性(以及其他几个属性)且其值为 none 以外的值,那么该父元素将成为绝对定位元素和固定定位元素的包含块。 这可以在这个示例中观察到,其中通知的 positionfixed;,而父元素具有 transform,但只有在悬停时才生效

查看 CodePen 上的示例 CSS-Tricks:包含块,作者是 Brad Westfall (@bradwestfall)。

问题 4:父级和首尾子级合并边距

这是 CSS 细节中的一种,如果您不知道它的工作原理,它会真的让您头疼。CSS 中有一个名为合并边距的概念,许多人熟悉其中的一种形式,称为相邻兄弟合并边距。但是,它还有另一种形式,称为父级和首尾子级合并边距,它鲜为人知。以下是对这两种情况的演示

查看 CodePen:CSS-Tricks: 合并边距,作者:Brad Westfall (@bradwestfall),来自CodePen

每个段落标签都具有浏览器提供的 1em 的顶部和底部边距。到目前为止,这部分很简单。但是为什么段落之间的间隙不是 2em(顶部和底部的总和)?这就是所谓的相邻兄弟合并边距。边距会重叠,因此两个边距中较大的那个将成为总间隙大小,因此在这种情况下,间隙为 1em。

还有一些其他的情况,看起来有点奇怪。您是否注意到第一个段落的顶部边距并没有在它和蓝色容器 div 之间产生间隙?它并没有产生间隙,而是像将边距“贡献”给了父级 div,就好像 div 具有顶部边距一样。这就是所谓的父级和首尾子级合并边距。如果父级具有以下任何内容,则这种形式的合并边距在某些情况下不会发生

  • 任何大于 0 的顶部/底部填充值。
  • 任何宽度大于 0 的顶部/底部边框。
  • 块级格式化上下文,可以通过诸如overflow: hidden;overflow: auto;之类的操作来创建)。
  • display: flow-root(支持性不佳)。

当我向人们解释这个 CSS 细节并使用填充或边框解决它时,他们的反应几乎总是“填充或边框为 0 怎么办?”,这行不通,因为值必须是正整数。

在前面的示例中,只需 1px 的填充就可以让我们在使用和阻止父/子级合并边距之间切换。第一个/最后一个段落和父级之间出现的间隙是 1px 的填充,但现在边距正在被纳入容器内部,因为填充层创建了一个阻止合并边距的屏障。

关于这个问题,我相信您能在这个 UI 中看到问题所在

查看 CodePen:CSS-Tricks: 父/子级合并边距,作者:Brad Westfall (@bradwestfall),来自CodePen

第一个.comment(没有.moderator类)正在经历合并边距。即使不查看代码,我们也可以看到主持人评论有一个边框,而非主持人评论没有。在这个问题中,实际上有三个被认为是正确的答案。每个答案实际上已经在 Pen 的源代码中应用了,只是被注释掉了。

这种形式的合并边距不像其他形式那么广为人知的一个原因是,我们可以通过多种方式“意外地”避免它。Flexbox 和网格项目会创建一个块级格式化上下文,因此我们不会在那里看到这种形式的合并边距。如果我们的“评论”UI 是一个真正的项目,我们很有可能在所有四个坐标上都设置了填充,以在周围创建间距,这将为我们修复任何合并边距问题。尽管它可能很少见,但我不想让你为这个问题挠破头皮一整天,所以当你处理布局时,最好将它铭记于心。

以下是一些关于这个主题的 CSS-Tricks 文章

问题 5:百分比是相对于什么?

在使用百分比单位时,据说百分比是基于包含块的宽度或高度(通常与父级相关)。如前所述,具有transform的元素将成为一个包含块,因此当元素使用transform时,百分比单位(仅针对transform)是基于它自身的大小,而不是父级。

在这个示例中,我们可以看到 50% 的含义取决于上下文。第一个红色块具有margin-left: 50%;,第二个红色块使用transform: translateX(50%);

查看 CodePen:CSS-Tricks - 百分比和变换,作者:Brad Westfall (@bradwestfall),来自CodePen

问题 6:盒子模型再次出现… 真让人头疼!

正当您以为我们已经结束讨论盒子模型的时候…

查看 CodePen:CSS-Tricks: 左:0 右:0,作者:Brad Westfall (@bradwestfall),来自CodePen

让人头疼的原因在于我们正在对页脚使用width: 100%;并添加填充。容器的宽度为 500px,这意味着页脚的内容层(为 100%)在应用到该层外部的填充之前为 500px 宽度。

可以通过以下两种常见技术之一解决这个问题

  1. 直接在页脚上使用box-sizing,或通过重置来使用,就像我们之前讨论的那样。
  2. 删除width,改为使用left: 0; right: 0;。这是同时使用left值和right值的一个很好的用例。这样做可以避免盒子模型问题,因为当设置left: 0; right: 0;时,width将使用其默认值auto来占用填充和边框之间的任何可用空间。

其中一个选项是“删除页脚的填充”。从技术上讲,这可以解决问题,因为内容层为 100% 不会有任何填充或边框将其扩展到容器的宽度之外。但我认为这种解决方案是错误的,因为我们不应该改变我们的 UI 来适应很容易避免的盒子模型问题。

对我来说,现实情况是我总是将box-sizing: border-box;作为我重置的一部分。如果您也这样做,那么您可能不太常遇到这个问题。但无论如何,我仍然喜欢使用left: 0; right: 0;技巧,因为随着时间的推移,它比处理width: 100%;对定位元素引起的盒子模型问题更加稳定(至少在我的经验中是这样)。

问题 7:居中绝对定位和固定定位的元素

现在我们真正开始将上面所有的材料与绝对定位和固定定位元素的居中结合起来

查看 CodePen:CSS-Tricks: 模态框(灯箱)居中,作者:Brad Westfall (@bradwestfall),来自CodePen

由于我们已经涵盖了本测试问题中大部分的材料,因此我只指出,水平和垂直居中可以通过使用负边距的“老方法”或使用变换的“有点老但是仍然有效”的方法来完成。以下是一个关于所有居中方面的CSS-Tricks 指南

曾经有人说过,如果我们知道盒子的宽度和高度,那么我们应该使用负边距,因为它们比过渡更稳定,过渡是浏览器的新功能。现在过渡已经稳定,我几乎一直使用它们来完成这项任务,除非我需要避免包含块。

另外,请注意,我们不能为此使用任何margin: auto;技巧,因为我们需要模态框“悬停”在内容之上,这就是为什么通常使用position将它们从普通流中移出的原因。

说到这里,让我们继续下一个问题,它与使用普通流居中有关。

问题 8:使用普通流居中元素

Flexbox 为我们带来了许多解决困难布局问题的惊人工具。在它发布之前,人们都说垂直居中是 CSS 中最难做的事情之一。现在,这已经变得很轻松了

.parent { display: flex; }
.child { margin: auto; }

查看 CodePen:CSS-Tricks: Flexbox 居中(垂直和水平),作者:Brad Westfall (@bradwestfall),来自CodePen

请注意,对于 flexbox 项目,margin: auto被应用于顶部、右侧、底部和左侧,以实现垂直和水平居中。在过去,使用auto进行垂直居中在块级元素上是无效的,这就是为什么使用margin: 0 auto很常见的原因。

问题 9:计算混合单位

当我们需要混合两个无法直接加起来的单位或需要使分数更容易阅读时,使用calc()是完美的选择。这个测试题要求我们根据 div 的宽度为 100px 的事实来计算calc(100% + 1em)的结果。这有点棘手,因为 div 的宽度实际上并不重要。百分比是基于父元素的宽度,所以答案是 _包含块(父元素)宽度的 100% 加上 1em_。

我经常使用calc()的几个关键地方。一个是在任何我想将某个元素偏移 100% 并添加固定数量的额外空间时。下拉菜单就是一个很好的例子

查看示例 CSS Tricks: 计算混合单位 by Brad Westfall (@bradwestfall) on CodePen.

这里的技巧是我们想创建一个“下拉系统”,下拉菜单可以使用不同的触发器大小(在本例中是两个不同大小的按钮)。我们不知道触发器的高度,但我们知道top: 100%; 会将菜单置于顶端,并位于触发器最底部。如果每个菜单都需要位于各自触发器的底部,加上 0.5em,那么就可以使用top: calc(100% + 0.5em); 来实现。当然,我们也可以使用top: 110%;,但额外的 10% 将取决于触发器和容器的高度。

问题 10:负边距

与将元素推离其兄弟元素的正边距不同,负边距会将兄弟元素拉得更近,**而不会移动兄弟元素**。这个最后的测试题提供了两个技术上有效的解决方案来消除按钮组中的双重边框,但我更喜欢负边距技术,因为移除边框将使执行某些技巧(如悬停效果)变得更加困难。

查看示例 CSS Tricks: 按钮组的负边距 by Brad Westfall (@bradwestfall) on CodePen.

效果是“公共边框”在按钮之间显示。按钮实际上无法共享公共边框,所以我们需要这个负边距技巧让两个边框重叠。然后我使用z-index来管理根据悬停状态想要置于顶部的边框。请注意,即使没有绝对定位,z-index在这里也很有用,但我必须使用position: relative。如果我使用了移除第二个按钮左侧边框的技术,这个效果将更难实现。

所有都加起来了!

还有一个演示我想向您展示,它使用了我们迄今为止讨论过的许多技巧。任务是创建 UI 图块,这些图块会扩展到容器的左右边缘,并带内边距。所谓“图块”,是指能够拥有一个块列表,当没有更多空间时会向下折行。悬停在图块上以查看完整效果

查看示例 CSS-Tricks: Flexbox 图块(边缘到边缘) by Brad Westfall (@bradwestfall) on CodePen.

这个任务的障碍是内边距。如果没有内边距,让图块接触容器的左右边缘将是微不足道的。问题是内边距将由边距创建,当我们在图块的四面都添加边距时,会产生两个问题。

  1. 使用三个width: 33.33%; 的图块加上边距意味着三个图块无法在一行内容纳。虽然box-sizing允许我们在.tile上添加填充和边框,这些边框将包含在33.33%内,但它不会帮助我们处理边距 - 这意味着三个图块的计算宽度将超过 100%,迫使最后一个图块向下折行。
  2. 最左边和最右边的图块将不再接触容器的边缘。

第一个问题可以使用calc((100% / 3) - 1em)解决。这是 33.33% 减去每个图块的左右边距。相邻兄弟元素合并边距在这里不适用,因为在左右边距中不存在合并边距。因此,每个图块之间的水平距离是两个边距的总和(1em)。在本例中,它也不适用于上下边距,因为第一个图块和第四个图块在技术上不是相邻兄弟元素,即使它们在视觉上恰好彼此相邻。

使用calc()技巧,三个图块能够在一行内容纳,但它们仍然没有扩展到容器的边缘。为此,我们可以在每个图块的左右边距的相同量级上使用负边距。示例中的绿色虚线是容器,我们将应用负边距以将图块拉伸以匹配周围内容的边缘。我们可以看到它如何扩展到其父元素的填充区域(main 元素)。这是可以的,因为负边距不会推动相邻元素移动。

最终结果是图块具有漂亮的内边距,这些内边距扩展到边缘,使其与图块外部的相邻段落标签对齐。

有很多方法可以解决图块问题(它们通常有各自的优缺点)。例如,有一个相当优雅的解决方案使用 CSS Grid,由 Heydon Pickering 讨论,使用模仿容器查询的技术(但使用 Grid 魔法)实现响应式布局。最终,他的 Grid 图块解决方案比我提供的 flexbox 解决方案好得多,但它也拥有较少的浏览器支持。尽管如此,flexbox 解决方案仍然是同时演示本文中所有技巧的好方法。

您可能已经熟悉 Heydon 的作品。他以创建像Lobotomized Owl 选择器这样的巧妙技巧而闻名。如果您不熟悉,它肯定值得了解,我有一个视频,我讨论了它。

总结

我在开头就说过,我倾向于在解决问题时寻找模式。这篇文章并不是关于上面提到的确切演示场景;它更多的是关于一组工具,这些工具可以用来解决这些问题以及我们都可能遇到的许多其他布局问题。我希望这些工具能帮助您走得更远,我期待在评论区听到您的意见。

顺便说一句,有一些优秀的资源详细介绍了盒模型,最著名的是 Rachel Andrews 和 Jen Simmons 的资源,这些资源绝对值得一看。Rachel 甚至有一个专门关注布局的新闻稿

  • 盒模型对齐速查表 – 有视觉效果的出色资源,突出了影响元素对齐方式的各种属性,无论是它们自身还是相对于其他元素。
  • Jen Simmons Labs – 一系列使用现代布局方法的有用文章、演示和实验。