帮助我们选择 CSS 中 Masonry 布局的最终语法

回到 2024 年 4 月,我们撰写了一篇关于 CSS 中“Masonry”布局的文章,以及将此功能引入浏览器的持续工作。在文章中,我们描述了一场争论,即 CSS Grid 的全部功能(子网格、跨度、明确放置以及轨道尺寸设定的所有选项)是否应与目前通过 masonry.js 等工具实现的紧凑布局相结合。一些人认为,完整的复杂性不是必需的,甚至不切实际,一个专注于解决经典 Masonry 用例的更简单功能会更好。人们对与 Grid 轨道尺寸设定的全部功能和灵活性集成是否可行存在非常实际的性能担忧。浏览器引擎能否在保持极快速度的同时处理 Grid + Masonry?

我们邀请您加入讨论,帮助我们。感谢所有分享他们的想法、用例、图表和演示的人。您的反馈很有价值,有助于我们推动对话。

在过去的六个月里,主要的性能问题已经得到解决。是的,可以将 Masonry 布局与 CSS Grid 的全部功能集成。我们在 Apple 的工程师一直努力与 Google 的同事以及 CSS 工作组 (CSSWG) 合作,共同探讨所有细节。

Demo of simply masonry-style layout, with numbers showing the order of items in the layout. Also, a header on the page shows the code used to make this layout, along with controls to try other variations.
我们 2024 年 4 月关于 Grid Level 3 的文章包含了 四个演示的详细讲解,共有 23 种变体。您可以在 Safari 技术预览版、Safari 17.x 或 18.x 中(在“开发”>“功能标志”中勾选“CSS Masonry Layout”后),或在 Firefox 77+ 中(启用其标志后)试用它们。

一旦我们证明这是可行的——并且在您的帮助下,将这一新功能与 Grid 的全部功能结合起来是个好主意——CSSWG 在 9 月份决定为瀑布流布局采用混合轨道尺寸设定。这是一个了不起的里程碑!基于这一共识,CSSWG 发布了 CSS 网格布局模块级别 3 的首个公开工作草案。

剩余的语法之争

此时,还剩下一个主要问题——语法应该是什么?目前,规范草案中包含两种选项,它们正进行直接竞争——“网格集成”选项和“网格独立”选项。(为了更清楚地说明哪个是哪个,在本文中我们将它们分别称为“直接使用网格”选项和“新瀑布流布局”选项。)

语法决定取决于我们如何构思此功能及其未来。它是 CSS Grid 的扩展,利用现有 Grid 属性吗?还是应该将其视为完全不同的事物,拥有自己一套新属性和新默认值?无论哪种方式,您都可以编写相同的布局。功能将是相同的。

最近 Chrome 团队的人员撰写了一篇关于他们观点的文章

Chrome 团队仍然认为,单独的瀑布流语法是最佳的前进方向。尽管我们之前文章中提到的最大性能问题已解决,但仍存在关于语法、初始值以及与网格结合的版本易学性方面的担忧。

Rachel Andrew 在她的博客文章中补充了支持新瀑布流布局选项的论点

我的观点是,就像 2020 年一样,将 masonry 定义为 grid 的一部分将是一个错误……好的默认值让事情更容易教学……好的默认值意味着更少的配置。

他们的论点是,如果此功能成为 CSS Grid 的一部分,开发人员将不必编写那么多代码。通过重置格式化上下文,全新的属性可以拥有更适合 Masonry 布局的新默认值,使其更快、更容易,尤其是在编写由 Pinterest 推广的经典 Masonry 布局时。您将无需覆盖为 CSS Grid 设计的默认值。

Chrome 团队的文章列举了许多并排的例子来证明他们的观点。这是第一个,用两种语法定义了一个对称的 3 列瀑布流布局

新瀑布流布局

.masonry {
  display: masonry;
  masonry-template-tracks: repeat(3, 1fr);
  gap: 10px;
}

直接使用网格

.masonry {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: masonry; /* final name TBD */
  gap: 10px;
}
(我们还认为值 masonry 应该被重命名。参见脚注 1。)

在他们所有的例子中,新瀑布流布局选项确实少用了一行代码(或需要更少值的简写)。能够编写更少的代码是一种宝贵的品质。

此外,或许将这个新功能想象成 display: masonry 会感觉更整洁。它符合网页布局的一个简单故事:“你有几种选择:流式布局、多列布局、网格布局、弹性布局和瀑布流布局。选择你想要的,然后使用它。”对于许多人来说,这看起来是最佳的前进方向,这完全可以理解。

当您查看像这样的孤立示例,两种替代方案正面交锋时——特别是如果您还没有学习 CSS Grid——新瀑布流布局选项可能看起来更美观、更简洁、更容易猜测其作用。

但是,一个真实的 CSS 文件绝不是五行代码。实际情况要复杂得多。您可能希望在某个断点之前编写一个经典的 Grid 布局,然后在更宽(或更窄)的屏幕上切换到瀑布流式布局。在这种情况下,与“直接使用网格”选项相比,“新瀑布流布局”选项需要更多的代码行才能切换布局。

Apple 的 WebKit 团队仍然坚信,将 CSS Grid 和瀑布流式紧凑布局分离成两个独立的布局机制将是一个错误。我们认为,孤立地查看简单的代码示例可能不是评估此决定的最佳方式。我们认为所有这些功能都应作为统一布局系统的一部分,并以统一语法实现最佳服务。在本文中,我们将深入探讨原因。

设计原则

每当出现像这样的艰难分歧时,最佳的前进方式是采取“3万英尺”的视角——讨论可用选择的更广泛影响。一个 W3C 工作组是如何做到这一点的?通过依赖设计原则。CSS 是一种发展了 30 多年的编程语言。在整个发展过程中,它都受到设计原则的指导。CSS 工作组没有像 HTML 那样记录官方的设计原则。但在 2003 年,CSS 的共同发明者 Bert Bos 写下了他对一个好的 Web 标准应该是什么样的想法。

这些原则包括:

  • 简洁性:好的编程语言拥有简单、易于理解的模型和语法。它们既易于使用,又足够强大,能够直接表达开发者的意图。复杂或实际使用起来繁琐的语言,难以理解和正确使用。
  • 可学习性:Web 技术应该易于开发者学习。它应该可读性强而不冗长;重用熟悉的语法来表达熟悉的概 念;并且,总的来说,应该为人类而非计算机设计。
  • 最小冗余:“不同规范之间的功能重叠应保持较小,因为它容易导致不兼容的模型。”虽然功能上的一些冗余可以帮助开发人员更清晰地表达逻辑,但不兼容的模型会使技术更难实现,并且更容易导致错误。
  • 重用:“将现有数据用于新目的”是 Web 设计的核心功能。HTML 设计原则称之为“不要重复造轮子”。扩展现有技术,而不是为相同或相似的目的发明新东西。
  • 利用现有:现有的 API 可能不理想。也许您希望我们能重新开始。但我们不能。利用现有的,开辟最佳前进道路。“抛弃虽然不完美但能运行的软件,并教大家新东西,将是对资源的巨大浪费。”
  • 可扩展性:CSS 被设计为可扩展的——其解析和解释规则、通用语法,甚至其属性及其值的具体语法——所有这些都经过有意设计,以适应 CSS 的未来扩展。它允许在较新的浏览器中对现有页面进行渐进增强,并在较旧的浏览器中对较新页面进行优雅降级。
  • 委员会设计:“规范由委员会而非个人创建……更多的眼睛意味着更多的错误检查、在寻找问题解决方案方面的更多创造力,以及在了解过去什么有效或无效方面的更多经验。”

换句话说,尽可能重用现有语言,并在过程中重新利用事物。尽可能保持简单。使其易于学习。不要在做事情上创建不止一种方式,避免冗余。并通过像这样的辩论来找到最佳解决方案!

让我们看看这些设计原则如何应用于当前的语法决策。

简洁性 | 可学习性

Chrome 团队成员提出的论点可以看作是在追求简洁性。作为开发者,如果能用少一行代码完成布局,难道不更简单吗?而且,如果可以直接使用新的 Masonry 属性的默认值,而无需覆盖它们,那肯定更简单。更少的代码。更好的默认值。更容易学习。对吗?

我们认为,将 Masonry 作为一个独立的显示类型,只有在孤立的情况下才显得更简单:比如在博客文章中阅读代码片段,专注于一对一的比较。但 Masonry 不会孤立存在。我们已经有了 CSS Grid。

让我们看看一个统一的布局系统会有怎样的体验。在 CSS Grid 中加入这项新功能会是怎样的?

Layout of 16 photos — all squares, in a 4 by 4 grid.A layout of 16 photos, of all different aspect ratios, packed into 4 columns, in a masonry-style layout.

要布局左侧所示的方形图片,您可以编写

main {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  grid-template-rows: auto; /* default value, unnecessary to state */
  gap: 1rem;
}

要布局右侧所示的、具有不同长宽比的图片,您将编写

main {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  grid-template-rows: collapse; /* final value name TBD */
  gap: 1rem;
}
(在本文中,我们的示例设想使用 collapse 作为值而不是 masonry。参见脚注 1。)

看看这有多简单。使用已经存在 7 年多的 CSS Grid 布局系统,只更改一个值。

让我们看另一个例子——这次是创建一个横向流动和滚动的布局。这对于手机或平板电脑用户来说可能感觉特别自然。

iPad with a layout of content flowing across it. Users will scroll it side to side. The content is a set of cards, each containing a painting and information about the painting. In this layout, every card is a rectangle, lining up in a columns as well as rows.

要创建此布局,我们今天可以使用 Grid

main {
  display: grid;
  grid-template: "tall" 2fr
                 "wide" 1fr;
  grid-auto-flow: column; /* fill by column */
  height: 600px;
  overflow: scroll;
}
.tall-item {
  grid-row: tall;
}
.wide-item {
  grid-row: wide;
}

但是,考虑到图片具有多种长宽比,我们不妨将内容紧凑排列,而不是让它们拉伸。使用“直接使用网格”选项,我们只需多写一行代码——grid-template-columns: collapse

Same iPad with content flowing across it, to scroll it side to side. This time, every card is a rectangle that is just big enough to fit the content, lining up in rows, but things to not line up in columns. It's a staggered layout.
main {
  display: grid;
   grid-template-columns: collapse;
   grid-template: "tall" 2fr
                  "wide" 1fr;
  grid-auto-flow: column; /* fill by column */
  height: 600px;
  overflow: scroll;
}

从考虑 CSS Grid 到考虑紧凑的瀑布流式布局,这是一个轻松的过渡,因为它都是同一系统的一部分。此外,我们可以在断点处轻松修改此布局。

现在,让我们对比一下使用“新瀑布流布局”选项的开发者体验。

main {
 display: masonry;
 masonry: "tall wide" 
           2fr  1fr;
 masonry-direction: row; /* grow along the rows */
 height: 600px;
 overflow: scroll;
}

“新瀑布流布局”模型可能在孤立情况下完全说得通,但在实际应用中,它要求开发者在不同的心智模型之间切换。它要求记住 Grid 和 Masonry 语法有何不同——向右流动是 row 而不是 column;水平而不是垂直地布局区域模板;将事物称为 -tracks 而不是 -rows-columns……这些差异累积起来。

开发者已经很难理解 Flexbox 和 Grid 之间的区别,以及何时使用哪一个。太多开发者为了应对这种负担,干脆对所有事物都只使用 Flexbox,从不使用 Grid。再增加一个布局模式很可能会加剧这一挑战。

输入四行代码而不是三行代码并不是一个显著的开发人员负担。而必须记住多套名称、允许值和默认值各不相同的相似语法,则是一个更大的负担。

最小冗余 | 重用 | 利用现有

驱动 CSS 工作组决策的指导原则之一是,在创建新可能性时,始终努力重用现有模式和属性。

例如——当 Flexbox 被发明时,它带来了对齐属性(justify-contentalign-items 等)。随后当 Grid 出现时,这些相同的属性被重用于相似但略有不同的目的。为了扩展功能,添加了与现有模式(align-items)匹配的新属性(justify-items)。

gap 属性也是如此。当多列布局被发明时,新的 column-gap 属性提供了一种定义列间距的方法。十年后,当 CSSWG 需要一种方法来定义网格列之间的间距时,column-gap 属性被重新利用。它得到了对应的 row-gap 属性和一个新的简写属性 gap,以使其更具通用性,最终也进入了 Flexbox。经过几次尝试,CSSWG 意识到为不同的上下文不断创建独立的间距属性(column-gap + grid-gap + flex-gap…)将是一个错误。最好是重新利用已有的东西。

“新瀑布流布局”选项为现有功能创建了一套重复的属性。(所有水蓝色部分都是新的)

直接使用网格选项 新瀑布流布局选项
display: grid display: masonry
grid-template-columns / grid-template-rows masonry-template-tracks
grid-template-rows: collapse /
grid-template-columns: collapse
masonry-direction: column /
masonry-direction: row
grid-template-areas masonry-template-areas
grid-template masonry-template
grid-auto-flow masonry-direction
masonry-fill
masonry-flow
gap gap
grid-column-start / grid-row-start masonry-track-start
grid-column-end / grid-row-end masonry-track-end
grid-column / grid-row masonry-track
grid-auto-columns / grid-auto-rows masonry-auto-tracks
grid masonry
grid-slack(名称待定) masonry-slack(名称待定)

它将要求开发者记住一个拥有完整第二套语法的并行布局系统。

“直接使用网格”选项*只*为 CSS 添加了*一个新值*。您只需在轨道定义中使用 collapse [1] 值即可创建瀑布流式布局:grid-template-rows: collapse(或在另一个方向使用 grid-template-columns: collapse)。基本上您是在“折叠”行——这对于各种用例都很有用。

通过不重用已有的内容,“新瀑布流布局”在 CSS 中造成了不必要的冗余。然而,Chrome 团队坚称创建重复系统是值得的,因为它允许通过三种方式针对瀑布流布局用例调整其细节

  1. 调整概念——例如使用 masonry-direction: column 而不是 grid-auto-flow: row——以便它们更好地匹配 Masonry 框架中的概念。
  2. 更改默认值,以便开发人员无需经常指定他们想要的内容。
  3. 引入目前 Grid 中不存在的专业功能,例如将 repeat(auto-fill, auto) 作为轨道定义。(我们将在本文后面讨论这一点。)

我们不认为增加 10 个冗余属性给开发者带来的负担,与所建议的收益是相称的。是的,新的 masonry-* 属性可以根据瀑布流特定用例进行定制。但开发者将不得不记住两个系统之间的所有差异,并掌握哪个是哪个。

Set of images in a staggered layout, each with a number labeling their DOM order. Current order 11, 9, 10, 12. Item 9 sticks up above the rest, higher on the page. Item 10 is a very tiny bit higher than item 11. Item 12 is slightly lower than the rest.
这两种选项都带有一个全新的属性——*-slack 属性——它允许开发人员告诉浏览器在确定下一个项目放置位置时的“挑剔”程度。如果没有 slack,项目 10 会在第三列,因为它在那里比放在最左侧的列稍微*高一点*。有了“slack”,可以告诉浏览器忽略这种“微小”的差异,从而使项目 10 位于最左侧,后面是项目 9、11、12。

CSS 工作组通常不会重复现有功能。相反,它会为新目的扩展现有功能。我们不相信这种情况提供了足够有说服力的理由来偏离标准方法。

可扩展性

CSS 工作组最艰难的工作是尝试预测未知的未来,并做出我们以后不会后悔的明智决定。这种担忧是关于 CSS 嵌套长期争论的一部分——例如,我们是否将自己限制在一个会阻碍未来扩展的角落里。(最终,我们为嵌套找到了一个极好的解决方案。)

在我们讨论这个决定——是扩展 CSS Grid,还是创建一个新的显示类型——时,我们应该仔细思考这个选择将如何影响未来。我们希望在未来建立哪种模式?

“直接使用网格”选项倾向于这样一种观点:CSS Grid 是网页布局的主要机制,我们应该继续扩展它,使其越来越强大。“新瀑布流布局”选项似乎在说,不,我们应该让 Grid 保持 Grid 的样子,每次增加更多布局功能时,都添加新的独立显示类型。

这是一个可能具有深刻哲学性且难以直接讨论的问题。您更喜欢哪个方向可能只是一种直觉。所以,让我们用一个真实的例子来代替理论问题。让我们想象一下未来我们可能添加到 CSS 布局中的其他内容。

未来可能的功能

想象您是一个充满文章的网站的前端开发人员。然后您的设计师发来这个

A typical article layout, with most content in a long wide column. Except headlines jut out to the left. Some images start further left, others extend to the right. Plus text flows around floated images.

您将如何在 CSS 中编写此布局?

十年前,您可能曾求助于负外边距。将所有内容放在一个主列中,然后使用 margin-left: -20px 这样的代码将某些内容(标题、图片)从主列中向左或向右拉出。

现在 99% 的用户都支持 CSS Grid,您有了更多选择。您仍然可以使用负外边距。或者,您可能会将 article 元素设置为 Grid 容器,并将每个标题、段落、图表等转换为 Grid 子项。这将使您能够将某些内容沿着特定的网格线对齐。但是使用 Grid 会带来其他问题。突然间,您会遇到“双重外边距”——您的顶部和底部外边距会堆叠在一起,而不是折叠。此外,您也无法再使用浮动。它们根本不起作用。article 的每个直接元素现在都是一个 Grid 子项,位于自己独立的行中。

如果相反,您可以利用 grid-template-columns 的优势,包括其网格线和明确放置内容的能力——同时又保留流式布局的优点,例如浮动、外边距折叠等。那感觉就像是布局文章的正确工具。

我们可以停留在流式布局上下文中,并且使用 CSS Grid 的特性,像这样

article {
  display: block; /* default value, unnecessary to state */
  grid-template-columns: 1fr 1fr minmax(15ch, 30ch) minmax(15ch, 30ch) 1fr;
  grid-default-column: 3 / 5;
}

一个新属性,也许命名为 grid-default-column,可以让我们定义 article 元素所有直接子元素的默认位置——在这个例子中,是从第 3 行到第 5 行。(事实上,这样一个属性在 CSS Grid 中对于许多其他用例,包括瀑布流式布局,都会非常方便。)

Same article, now with vertical dotted lines drawn to mark where the CSS Grid lines could go.

一旦我们有了网格线,我们就可以定义我们希望内容放置在其他位置的布局。

h1, figure {
  grid-column: 1 / 6;
}
h2, h3 {
  grid-column: 2 / 6;
}
figure.left {
  grid-column: 2 / 4;
  float: left; 
}
figure.right {
  grid-column: 4 / 6;
  float: right; 
}

这是一个潜在的激进想法,即使用 grid-template-columns 来定义列,而创建网格格式化上下文。(这有点像您现在如何在流式布局中使用对齐属性。您不再需要创建 Flexbox 或 Grid 布局上下文来使用 align-content。)

这只是 CSS 工作组未来可能讨论的一个想法的例子。

通过为瀑布流式布局选择“直接使用网格”选项,这似乎是在肯定这样的想法。让我们继续扩展 CSS Grid(我们当前的布局系统)的可能性。让我们扩展 CSS Grid 来实现瀑布流式紧凑布局。让我们利用 CSS Grid 的一部分来处理流式布局。让我们继续为我们已有的布局系统创建更多功能。

“新瀑布流布局”选项似乎在说,好吧,任何时候我们想要创建另一种布局模式,都应该通过创建另一个新的格式化上下文来实现。这个新颖的想法不应该是 display: block; grid-template-columns: [track sizes]。相反,它应该是一个独立的布局系统,拥有自己一套新的属性和新的默认值,更好地匹配当前的目的。也许像这样

article {
  display: pillar;
  pillar-template-columns: 1fr 1fr minmax(15ch, 30ch) minmax(15ch, 30ch) 1fr;
  pillar-default-column: 3 / 5;
}

如果 CSSWG 朝这个方向发展,我们最终将拥有三套网格布局属性。下次再有其他想法时,我们是否会觉得被迫创建同一属性的第四个副本?

网格 瀑布流 柱状 另一个未来功能
display: grid display: masonry display: pillar display: foobar
grid-template-columns masonry-template-tracks pillar-template-columns foobar-template-baz
grid-column masonry-track pillar-column foobar-qux
grid-template-areas masonry-template-areas pillar-template-areas foobar-template-areas
……等等…… ……等等…… ……等等…… ……等等……

实际上更简单的是拥有一套语法来学习、记住和使用——即使有时它需要四行代码来完成三行代码可以做的事情,因为您需要声明所需的值,而不是依赖于自定义的默认值。

此外,新的 pillar-default-column 属性是否只会作用于 Pillar 布局内部,而不会扩展到 Grid 或 Masonry——导致这些独立的系统随着时间的推移不断分歧?

Chrome 团队认为,Grid 和 Masonry 作为独立的布局模式会更好,这样它们就不需要强制共同演进。他们认为,这将更容易向 Masonry 添加专业功能,而无需费心考虑新功能是否“也”适合添加到 Grid,反之亦然。

我们认为这些布局模式相互交织会更好。我们希望 CSSWG 能够仔细考虑新增功能——例如 grid-default-column——并使其适用于原始的 Grid 用例、瀑布流式布局用例,以及未来出现的任何其他情况。我们不希望一项新功能因为在某种模式下更容易实现,而被排除在另一种模式之外。我们希望 CSS 成为一个一致、连贯、可预测的系统。

使用 repeat(auto-areas, auto) 扩展 Masonry

这就引出了 Chrome 认为为 Masonry 创建一个重复属性系统的最后一个原因(上述三个原因之一)——因为这样就有可能引入目前 Grid 中不存在的专用功能。他们提议为 Masonry 添加三个新的轨道定义——repeat(auto-areas, auto)repeat(auto-fill, auto)repeat(auto-fit, auto)。第一个将是全新的默认值

.masonry {
  display: masonry;
  masonry-template-tracks: repeat(auto-areas, auto); /* new default value */
}

这些新的轨道值令人兴奋,因为人们相信它们将提供一种极其简单的方式来创建更少代码的瀑布流布局。每列的宽度(或每行的高度)将根据内容的大小自动计算。列数(或行数)也可以自动计算。

想象一下,作为开发人员,您不必描述列数或列的大小……只需将 display: masonry 应用到您的容器。浏览器会查看您的内容,计算所有尺寸,生成所需的列数,然后奇迹发生!您将获得一个经典的瀑布流布局,就像 Pinterest 一样,只需一行代码。还有什么比这更容易学习和使用的呢?这听起来确实很有前景!但我们不相信 masonry-template-tracks: repeat(auto-areas, auto) 真的能提供预期的体验。

实际上这里有两个功能,它们是相互关联的。新的默认值,以及当第一个条件的条件不满足时成为默认值的后备方案。我们对两者都有担忧。让我们一个一个地解决它们。

警告!本文的这一部分将深入探讨越来越难以理解的概念。这是我们对当前“新瀑布流布局”选项提案的担忧。开发人员必须理解 auto 尺寸设定(以及我们即将解释的其他概念),才能自信地使用它。而这些东西并不容易理解。但是,嘿,让我们试试……

首先,是新的默认值。值 repeat(auto-areas, auto) 会自动将 auto 分配为列的大小,列数取自 masonry-template-areas

.masonry {
  display: masonry;
  masonry-template-areas: "a b c";

  /* default value, so you don't need to state it */  
  masonry-template-tracks: repeat(auto-areas, auto);
}

这是一个非常好的主意。实际上,CSS Grid 已经具备了此功能

.grid-with-auto-sizing {
  display: grid;
  grid-template-areas: "header header"
                       "main  sidebar"
                       "footer footer";

  /* default value, so you don't need to state it */ 
  grid-auto-columns: auto; 
}

今天,CSS Grid 从 grid-auto-columns 中获取“缺失”的列大小,而 grid-auto-columns 的默认值是 auto。没有必要再次发明 repeat(auto-areas, ...) 来提供相同的功能,并且使用完全不同的语法。

据推测,创建此机制的原因是为了让 masonry-template-tracks 设置一个吸引人的备用默认值,当没有定义区域时会生效。

使用 repeat(auto-fill, auto) 扩展 Masonry

masonry-template-areas 未被使用时,auto-areas 行为被定义为回退到 auto-fill。由于大多数布局不定义区域,因此“新瀑布流布局”选项的功能默认值是 masonry-template-tracks: repeat(auto-fill, auto)正是支持者期望能够自动创建 Pinterest 布局的代码。

这里有一个非常有趣的想法。我们能否让浏览器通过仅查看内容尺寸——而无需来自 Web 开发者的任何信息——来确定创建多少列以及这些列的大小?

如今的 CSS Grid 中还没有类似的功能。以前这被认为是不可行的,因为浏览器在知道轨道大小之前无法计算轨道,并且在完成放置之前无法确定轨道大小,但它需要知道轨道数量才能进行放置。(这会创建一个不可能的循环。)当 Chromium 团队讨论 Masonry 时,他们意识到通过做出一些足以应对典型用例的假设,这实际上是可能实现的。

如果您使用过 CSS Grid,您可能编写过 repeat(auto-fill, minmax(200px, 1fr) 来告诉浏览器创建所需数量的列以填充可用空间,其中每列至少 200px 宽且具有弹性。但是 repeat(auto-fill, auto) 会做什么呢?

让我们来看一个例子。假设我们有一个图片页面需要用 Masonry 布局。我们正在使用 CMS 或 CDN 将所有图片文件自然调整为 600px 宽,高度各不相同。

<article class="container">
  <figure><img width="600" height="500" alt="[description]"></figure>
  <figure><img width="600" height="300" alt="[description]"></figure>
  <figure><img width="600" height="150" alt="[description]"></figure>
  <figure><img width="600" height="750" alt="[description]"></figure>
  <figure><img width="600" height="400" alt="[description]"></figure>
  <figure><img width="600" height="250" alt="[description]"></figure>
  <figure><img width="600" height="375" alt="[description]"></figure>
  <figure><img width="600" height="360" alt="[description]"></figure>
  <figure><img width="600" height="450" alt="[description]"></figure>
  ...etc...
</main>

我们希望图片具有弹性,所以我们应用了一种经典的响应式设计技术。

img {
  width: 100%;
}

有了新瀑布流布局的提议默认值,我们应该能够用很少的代码创建瀑布流式布局!这就是我们想要的魔法。我们只需编写

.container { 
  display: masonry;
  gap: 10px;
}

那么浏览器如何决定列的宽度?以及创建多少列?

它会查看图片,发现它们宽 600px,并假定每列至少应宽 600px。然后它会计算有多少这样的列能容纳在可用空间中。

假设在某个时刻,用户将浏览器窗口调整为 1600px 宽,并且在整体页面布局下,.container 的宽度为 1410px。这意味着有足够的空间容纳两列 600px 宽的图像,其中包含 10px 的间隙,并剩下 200px。作为开发人员,您可以使用对齐属性来决定如何处理这些额外的空间——起始、结束、居中、等间距等。默认情况下,额外的空间将分配给各列,因此我们得到两列 700px 宽的列。

Two very compressed & grainy images next to each other in a layout of a web page.

您可能已经看到了问题所在。我们不希望我们的列这么大。图片的宽度是 600px,预期它们会被缩小,以便在 2x 和 3x Retina 屏幕上看起来很棒。然而,我们的图片却被拉伸以填充 700px 宽的列,使其看起来非常糟糕。我们声明了图片的 width: 100%,但没有任何东西“推动”它们使其小于其自然尺寸。这意味着它们将以 1 倍或更低的分辨率显示。

我们可以尝试对图片设置最大尺寸

img {
  width: 100%;
  max-size: 200px;
}

现在,图片在 Retina 屏幕上会好看很多,但浏览器只会将所有图片固定在 200px 宽,而不是弹性尺寸。它们将以最大尺寸显示,位于通常更大的列中。这绝对不是我们想要的结果。

Five columns of images, where the image does not fill the whole column. Odd extra space is marked with light grey.

让我们假装我们有一种方法可以直接告诉浏览器以 3 倍而非 1 倍显示图像。很久以前,CSS 曾提出一个 image-resolution 属性(现已废弃)。让我们思考一下如果我们这样设置图像大小它将如何工作:

img {
  width: 100%;
  image-resolution: 3dppx;
}

浏览器会计算尺寸,就像这些图像是 200px 宽一样(实际上它们是 600px,以 3 倍显示)。这将导致流体列的宽度为 200px 或更大,不会太大,其中包含填充列的流体图像,分辨率介于 2 倍和 3 倍之间。太棒了!这就是我们想要的。

但这种技术只有在所有图片自然宽度都为 600px 时才有效。如果它们来自一个控制力较弱的后端,宽度各不相同,那么列的最终宽度将是最大图片宽度除以 3。您无法预测结果。如果最大图片宽度为 1500px,那么列的宽度将从 500px 开始。如果最大图片宽度为 2400px,列的宽度将从 800px 开始。列的尺寸取决于哪些图片恰好加载。此外,image-resolution 目前还不存在。

让我们尝试一个不同的想法。我们可以为图片设置一个固定宽度以用于列尺寸设定,然后用 min-width 覆盖该尺寸,使图片具有弹性。这有点意想不到的反向逻辑,但它确实有效。

img {
  width: 200px;
  min-width: 100%;
}

浏览器将根据可用空间创建任意数量的流体列,只要这些列的宽度在 200px 或更宽。图片将是流体式的,填充列宽。布局看起来就像 Pinterest!

哦,等等……这只适用于内容仅包含图片,或图片带有一小段文字的情况。如果内容包含任何足够长以至于换行的文本,那么我们就会遇到一个新问题。

每当浏览器使用 auto 根据其内容大小调整列大小时,它都会尝试适应该内容的最大可能大小。对于文本,max-content 大小是文本字符串的整个宽度,不进行任何换行。想象一下这个段落被拉伸成一行。那是一个非常宽的框。

当匿名用户访问网站时,Pinterest 本身会在每张图片下方放置文本。让我们想象一下,“新瀑布流布局”选项及其提议的默认值将如何处理每个项目都是一张带有图片和标题的卡片的内容。

<main class="container">
  <article class="item">
    <img width="600" height="450" alt="[description]">
    <h2>Coffee</h2>
  </article>
  <article class="item">
    <img width="600" height="700" alt="[description]">
    <h2>We love traveling to get a great cup of coffee, no matter how far</h2>
  </article>
  ...
</main>

第一个标题“Coffee”的宽度可能小于 200px。如果图像尺寸设置为 200px 宽,并且标题为 80px 宽,那么此内容的最大宽度为 200px。没问题。

第二个标题“我们喜欢旅行去喝一杯好咖啡,无论多远”的宽度可能超过 200px。如果图像设置为 200px 宽,标题为 676px 宽,那么此内容的最大宽度为 676px。

浏览器将查看所有卡片,找出哪张卡片具有最大的最大尺寸,并使用其最宽的宽度来计算所有列的宽度。在这种情况下,所有列的宽度将为 676px 或更宽,最大可达 1351px。这不是我们想要的结果。

我们可以通过也给标题设置一个尺寸来弥补这一点,迫使它换行。要做到这一点,我们需要弄清楚使用哪些属性……widthmin-widthmax-width,还是组合使用?……以及设置哪些值……200px?更大?……小测验!您能弄明白吗?

或者,让我们退一步。更好的策略可能是将控制尺寸设定的代码应用于 .item 包装器,而不是其内部内容。不过,我们仍然需要使用 width: 200px + min-width: 100% 的技巧使其具有弹性。它不会默认自行拉伸。这样做意味着对齐属性(在 Grid 中非常方便)不再起作用,因为我们已经给项目明确设置了 100% 的尺寸。这也意味着,如果我们想为项目添加外边距,我们必须手动从 100% 中减去它们,就像在基于浮动的布局时代所做的那样。向我们的老“亦敌亦友” calc(100% - var(--margin-size)) 打个招呼吧。

了解其工作原理绝非易事!它需要对 auto 尺寸设定如何工作有深入的理解——这可以说是网页布局中最难的部分。提议的默认值通常不会“只用一行代码”就能神奇地生效。作为开发人员,您仍然需要完成所有控制轨道尺寸设定的工作。所需的 CSS 只需应用于项目和/或其内容,而不是布局容器。这回到了所有基于浮动的布局时代的工作方式——那时我们通过调整内容大小来控制布局。

CSS Grid 通过允许我们创建容器控制尺寸设定的结构,极大地改善了开发者体验。轨道尺寸设定比项目尺寸设定更强大——有更多方式定义交互。此外,通过要求浏览器扫描所有项目以查找其尺寸,然后计算轨道尺寸,它失去了直接从已定义的轨道值读取尺寸的性能优势。

但所有这些并不意味着这个想法永远不会有用。我们的巨型菜单演示(一个链接页脚的例子)是 repeat(auto-fill, auto) 可能有用的一个良好用例。文本字符串很短,无需担心图片尺寸,并且设计师可能希望避免换行。自动尺寸的列将产生极佳的结果,而无需开发人员调整内容尺寸。

a menu with a ton of links, like in a website footer, laid out with Grid Level 3

然而,布局短文本串并不是最常见的用例。而且正如我们所见,repeat(auto-fill, auto) 值对于最常见的用例来说并不实用。将其设为默认值没有意义。

此外,如果 CSSWG 认为这足够有用,为什么不让它适用于所有 Grid 布局,而不仅仅是瀑布流式布局呢?我们为 Grid 语法的其余部分找到了可行的定义,而 Chrome 团队最初认为这些定义在 Masonry 中是不可行的,所以我们不妨也尝试在 Grid 中找到一个可行的 repeat(auto-fill, auto) 定义,而不是制造分歧。这样做对开发者更好——更多的功能,更高的一致性,以及一个更易于学习的统一功能集。

我们希望 CSS 布局是一个统一且一致的系统。在 repeat 轨道定义中添加对 auto 的支持,很好地说明了为什么瀑布流式布局功能应该成为 CSS Grid 的一部分,这样新的想法才能整合到所有布局中,而不仅仅是其中一部分。

总结

单独来看,Masonry 作为一种独立的显示类型可能很有吸引力。它可能感觉更理论上纯粹。但当考虑将其整合到 CSS 的整个布局系统中时,它并不是最好的主意。我们必须考虑具有真实内容的真实网站。我们必须想象这个新功能将如何存在于数千行代码中并随断点变化。我们还应该思考我们的选择将如何影响 CSS 中未来的布局可能性。

CSS 设计原则应该指导这一决策,提醒我们重视真正的简洁性和可学习性。努力重用已有的内容,实现最小冗余。在可扩展性方面做出明智的架构决策——这些决策能够扩展一个集成化的功能网络,使所有功能都可预测地协同工作。

委员会设计

这就是您发挥作用的地方。作为一名 Web 开发人员,您可以帮助 CSS 工作组做出此决定。

我们很乐意听取您的想法。如果您能绘制一个真实世界的设计图,并写出两种选项的所有代码,那将非常有帮助。看看您更喜欢哪种实际使用此功能的方式。尝试同时使用经典的 CSS Grid 和创建瀑布流式布局的提案,看看将它们一起使用时感觉如何,无论是嵌套其中之一还是在断点处进行切换。在实际使用代码之后做出这个决定,比仅仅抽象地思考要好得多。

我们认为这个语法决策应该在深入研究实际示例时做出,因此我们在 webkit.org/demos/grid3 创建了相当多的示例。在 Safari 技术预览版中测试它们,其中“直接使用网格”选项默认开启。或者在 Safari 17.x 或 18.x 的任何版本中,通过在“开发”>“功能标志”中勾选“CSS Masonry Layout”来开启它。或者在 Firefox 中测试,方法是在 URL 中输入 about:config,同意风险,搜索“Masonry”,然后单击右侧的图标以启用其标志。(使用子网格的演示可能在 Firefox 中无法正常工作,但核心功能会正常。)

您可以在此处留下评论。或者更好的是,撰写您自己的文章,展示您的示例——然后将 URL 发布在评论中。目前,深思熟虑的定性反馈比单纯的大量简短意见更有帮助。


1. 脚注: 正如我们之前的文章所述,“masonry”不是一个理想的名称,因为它代表的是一种隐喻,而不是对其目的的直接描述。它也不是这种布局的通用名称。许多开发者将其称为“瀑布流布局”,这也是一种隐喻。

许多人已经提出了更好的命名建议。其中两个脱颖而出:collapsepack,例如 grid-template-rows: collapsegrid-template-rows: pack。您更喜欢哪个?或者您有其他建议吗?请就此问题专门评论一个新值名称(针对“直接使用网格”选项)。

在本文中,我们一直使用 grid-template-rows: collapse 来帮助想象 CSS Grid 集成瀑布流式布局新功能后的样子。同时,grid-template-rows: masonry 是您目前在 Safari、Safari 技术预览版和 Firefox 中测试演示时应该使用的。