我喜欢 CSS Grid。我喜欢它如何仅用几行代码,我们就能实现完全响应式的网格布局,通常无需任何媒体查询。我对使用 CSS Grid 来创建有趣的布局非常满意,同时保持 HTML 标记简洁干净。
但最近,我遇到了一个独特的 UI 难题需要解决。本质上,任何给定的网格单元格都可以有一个按钮,该按钮可以打开另一个更大的区域,该区域也是网格的一部分。但这个新的更大的网格单元格需要
- 正好位于打开它的单元格下方,并且
- 宽度全屏。
事实证明,它有一个很好的解决方案,并且本着 CSS Grid 本身的精神,它只涉及几行代码。在本文中,我将结合三个单行 CSS Grid“技巧”来解决这个问题。完全不需要 JavaScript。
我需要解决的实际问题的解释
这是一个我需要执行的最小 UI 示例
这是我们在 Storybook 组件库中渲染的实际产品卡网格

每个产品卡都需要添加一个新的“快速查看”按钮,以便在点击时,它会
- 动态地“注入”一张新的全屏卡(包含更详细的产品信息)到点击的产品卡下方,
- 而不影响现有的卡网格(即保留 DOM 源代码顺序以及浏览器中渲染的卡的视觉顺序),并且
- 仍然完全响应。
嗯……这在我们当前的 CSS Grid 实现中可能吗?
我肯定需要求助于 JavaScript 来重新计算卡的位置并移动它们,尤其是在浏览器调整大小的时候?对吗?
谷歌帮不了我。我找不到任何能帮助我的东西。即使搜索“快速查看”的实现也只产生了使用模态或覆盖层来渲染注入卡的示例。毕竟,模态通常是这种情况的唯一选择,因为它让用户专注于新内容,而无需打扰页面的其余部分。
我睡了一晚上,最后通过结合 CSS Grid 中一些最强大、最有用的功能,找到了一个可行的解决方案。
CSS Grid 技巧 #1
我已经在我们的默认网格系统中使用了第一个技巧,并且产品卡网格是该方法的一个特定示例。这是一些(简化的)代码
.grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, 20rem);
}
这段代码中的“秘密酱料”是grid-template-columns: repeat(auto-fit, 20rem);
,它为我们提供了一个网格,其中包含在可用空间中自动排列的列(在此示例中为20rem
宽),当没有足够的空间时会换行。
好奇auto-fit
和auto-fill
的区别吗?Sara Soueidan 已经写了 关于如何工作的精彩解释。Sara 还解释了如何将minmax()
合并以使列宽“灵活”,但是为了本文的目的,为了简单起见,我想要定义固定列宽。
CSS Grid 技巧 #2
接下来,我必须为网格中新的全屏卡提供空间
.fullwidth {
grid-column: 1 / -1;
}
这段代码有效,因为技巧 #1 中的grid-template-columns
创建了一个“显式”网格,因此可以为.fullwidth
卡定义起始列和结束列,其中1 / -1
表示“从第 1 列开始,跨越所有列直到最后一列。”
太棒了。一个注入网格的全屏卡。但是……现在全屏卡上方有间隙。

CSS Grid 技巧 #3
填补间隙——我之前用过一种仿砌体的方法
.grid {
grid-auto-flow: dense;
}
就是这样!需要的布局已实现。
grid-auto-flow
属性控制 CSS Grid 自动放置算法的工作方式。在这种情况下,dense
填充算法尝试填充网格中较早的空洞。
- 我们所有的网格列都具有相同宽度。如果列宽是灵活的,例如使用
minmax(20rem, 1f)
,密集填充也能工作。 - 我们每行中的所有网格“单元格”都具有相同高度。这是 CSS Grid 的默认行为。网格容器隐式具有
align-items: stretch
,导致单元格占据可用行高度的 100%。
所有这些的结果是,我们网格中的空洞被填补了——最棒的是,原始源代码顺序在渲染输出中得以保留。从可访问性的角度来看,这一点很重要。
请参阅 MDN 以详细了解 CSS Grid 自动放置的工作方式。
完整的解决方案
这三个技巧组合在一起提供了一个简单的布局解决方案,它只需要很少的 CSS。不需要媒体查询,也不需要 JavaScript。
但是……我们仍然需要 JavaScript 吗?
是的,我们需要。但不是为了任何布局计算。它纯粹是用于管理点击事件、焦点状态、注入卡显示等方面的功能性。
出于原型演示的目的,全屏卡已在 HTML 中以硬编码形式,位于 DOM 中的正确位置,并且 JavaScript 只需切换其显示属性。
但是,在生产环境中,注入的卡可能会使用 JavaScript 获取并在正确位置放置。像电子商务网站上的产品这样的网格布局往往具有非常重的 DOM,我们希望避免通过添加大量额外的“隐藏”内容来进一步无谓地增加页面大小。
快速查看应该被视为一种渐进增强,因此如果 JavaScript 无法加载,用户只会跳转到相应的商品详情页面。
可访问性考虑
我对使用正确的语义 HTML 标记、在绝对必要时添加aria-
属性以及确保 UI 仅使用键盘以及在屏幕阅读器中都能正常工作充满热情。
因此,以下是对使此模式尽可能易于访问的考虑因素的概述
- 产品卡网格使用
<ul><li>
结构,因为我们显示的是产品列表。因此,辅助技术(例如屏幕阅读器)将了解卡之间存在关系,并且用户将被告知列表中有多少个项目。 - 产品卡本身是
<article>
元素,带有适当的标题等。 - 当注入
.fullwidth
卡时,卡的 HTML 源代码顺序得以保留,从而为注入的内容提供了一个良好的自然选项卡顺序,并从注入的内容回到下一张卡。 整个卡网格都包装在一个.aria-live
区域中,以便将 DOM 更改通知屏幕阅读器- 焦点管理确保注入的卡
接收键盘焦点,并且在关闭卡时,键盘焦点将返回到最初触发卡可见性的按钮。
虽然原型中没有展示,但这些额外的增强功能可以添加到任何生产实现中
- 确保注入的卡在获得焦点时具有适当的标签。这可以像在内容中将标题作为第一个元素一样简单。
- 将ESC键绑定到关闭注入的卡。
- 滚动浏览器窗口,使注入的卡在视窗内完全可见。
总结
所以,你觉得怎么样?
这可能是一个很好的替代方案,可以替代模态窗口,让我们在想要显示额外内容时使用,但不会在过程中劫持整个视窗。这在其他情况下也可能很有趣——想想图片网格中的图片说明、辅助文本等等。它甚至可能替代某些情况下我们通常会使用 `<details>`/`<summary>` 的情况(正如我们所知,它们只在 特定环境 中使用最佳)。
无论如何,我对您如何使用它,甚至如何以不同方式处理它很感兴趣。请在评论中告诉我!
更新
首先,我真的很高兴这篇文章对其他前端开发人员很有帮助。我知道我不会是唯一遇到类似困境的人。
其次,在收到一些建设性反馈后,我在上面的一些具体的无障碍考虑方面添加了删除线,并在我的 CodePen 演示中添加了以下更改
- 卡片网格不需要包装在 `aria-live` 区域中。相反,我让快速查看打开和关闭按钮的行为像“切换”按钮,并使用相应的 `aria-expanded` 和 `aria-controls` 属性。我确实将这种模式用于披露小部件(显示/隐藏、选项卡、手风琴),但在这种情况下,我设想的是更类似于模态界面,尽管是内联而不是覆盖。(感谢 Adrian 提出的建议!)
- 我不再以编程方式将焦点放在注入的卡片上。相反,我只是添加了一个 `tabindex="0"`,这样键盘用户可以选择是否移动到注入的卡片上,或者他们也可以再次关闭“切换”按钮。
- 我仍然认为,对于产品卡片的列表,使用 `<ul><li>` 结构是一个合适的方法。提供的语义表明卡片之间存在明确的关系。
我的天哪,我刚用网格完成了类似的事情……以一种更像是“一些媒体查询加上一些负边距和一些 SCSS 函数”的方式。它可以工作,但不是那么干净。我会试一试……
祝你好运!是技巧 #1 中的 `auto-fit` 部分使这种网格布局无需媒体查询就能工作。
很棒的文章 Kev。说来也巧,我正在研究在一个新的 PLP 上做类似的事情——所以这非常有用!
谢谢……很高兴能帮上忙!;)
这是一个很棒的解决方案!非常感谢。
关于在 `article` 元素中使用适当的标题等,规范 指出
因此,可能存在更适合用于标记网格的元素。
谢谢 Ian。
关于 `<ul><li>` 结构,我已经更新了文章。
我们对所有卡片组件(产品/新闻/等等)使用 `<article>` 元素,因为它们可以在其他环境中使用或发布。是的,如果卡片的语义不能正确传达,这很可惜,但我认为列表的语义对于这种用例来说更为重要。
非常棒的文章!收藏了,以后参考。
厉害。这很酷。
CSS 网格布局方法很巧妙。阅读和焦点顺序都至关重要,我很高兴你正在考虑这一点。
但是,有一些无障碍方面需要调整
我将您的笔分叉以展示所有这些功能
我只在 JAWS 2021 / Chrome 94 中进行了快速测试,因为时间很晚,但我会在进一步检查后进行调整(并使用笔记进行更新)。
感谢 Adrian 的专业反馈。我已经更新了文章和 Codepen 示例,以使用“切换”按钮并删除动态区域。
当我最初制作这个解决方案的原型时,我将其视为一个“内联模态”,因此没有考虑采用“披露小部件”模式,我已经在我们的 UI 中其他地方用于选项卡/手风琴/等等。
我仍然保留“一次只显示一张注入的卡片”的行为……这是业务需求。就像注入的卡片中的“关闭”按钮一样,它将包含大量产品详细信息和其他交互式元素(例如分享和保存按钮)。
Kev,你走在正确的道路上,但如果不是屏幕阅读器用户/测试人员,很难知道一些陷阱。
在您展示的上下文中保留关闭按钮是有意义的。我只删除了它来展示 `aria-expanded` 控件的效用。
我仍然喜欢网格方法,并期待有机会使用它。谢谢!
我在 CSS 中看到 `li {list-style-type: none; }`,据称这会为某些 AT 删除此类语义。[1]
在这种情况下,是否建议使用显式 `<ul role="list">`?
[1] https://www.scottohara.me/blog/2019/01/12/lists-and-safari.html
感谢您发布有关列表语义的文章。我之前没有意识到这种特定的 Voice Over / Safari “bug”。
我必须承认,我不想在真实列表中添加 `role="list"` 来取悦一个屏幕阅读器。;)
恕我直言,如果屏幕阅读器没有宣布“N 项的列表”,我认为标记中仍然有足够的语义来帮助用户浏览产品。
哦……做前端开发的“乐趣”!!!
不要默认添加 `list` 角色。正如我在链接的帖子中所说,“除非用户测试表明您真的需要列表,否则缺少列表语义可能无关紧要”。通常情况下,它并不重要。
哇!太棒了!我在过去用大量 JavaScript 实现了这种功能。 https://yspisfy.com
……下次,我会使用这种方法。感谢您为我们所有人思考!
谢谢……我真的以为我必须为此添加大量 JS 才能让它工作!这只是说明了 CSS 网格有多棒!
感谢您提到网格的默认属性。感谢您说明网格在代码每次更改时如何变化。这很容易理解,无需在脑海中进行可视化。
谢谢。我必须真诚地感谢那些详细记录 CSS 网格工作原理的出色开发人员……无论是在 CSS Tricks 还是 MDN 上。
我使用 JS 完成了这种显示方式,但没有使用网格或弹性盒,虽然它可以工作并且看起来不错,但实现起来很痛苦。
查看
http://portal2web.biz/webs/a15_sfcb/our-brands/
当浏览器调整大小后,我必须不断地为元素添加或删除类,以便知道哪些项目构成一行,然后我必须注入一个对象来保存内容,并在关闭后将其删除等等。
我尝试过 CSS 网格,但只进行了一部分,甚至在 Reddit 的 /r/web_dev 板块上询问过,但没有收到任何回复。
非常感谢您提供这个非常巧妙的解决方案,它可以正常工作,感谢您与我们分享它。这真的让我的一天!
不客气!就像您一样,我也在 Google 上搜索过,但没有找到答案。我真的很惊讶,将 3 个“技巧”组合在一起就解决了问题!
不错!正是我需要的。有没有办法让网格项目拉伸以填满水平空间?谢谢。
好的,我想我明白了;)
grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
这真是太棒了,谢谢
不错的文章。我有一个客户的设计师要求做这个,我想不出一个好的方法(不涉及大量的布局 JS),所以我们不得不采用完全不同的方法——模态框。我喜欢认为我对 CSS 网格很拿手!哎呦。我会把这记在心里。
非常感谢!!
很棒,非常有用,这篇文章扩充了我的脑洞 :-)
作为一个承认自己避免 JavaScript 的人,我一直更喜欢完全没有 JS 的解决方案。而且——这是可能的,而且不会有太多不可见的开销。我的解决方案涉及一堆不可见的单选按钮,一些带有“for”属性的标签,以及 CSS 的同级选择器。有人感兴趣吗?
我也在适当的时候避免 JavaScript,把它作为一种渐进式增强。
快速视图通常在模态框中渲染。JavaScript 是必需的。
在这个特定的“内联”实现中,ARIA 公开模式 很合适,并且只需要少量的 JavaScript 来切换
aria-expanded
和管理focus()
。我以前见过单选按钮用于公开小部件(例如标签),但我认为屏幕阅读器中的反馈并不理想。在我看来,有明确说明按钮已展开的反馈更好。
我不建议这样做。辅助技术 (AT) 用户往往依赖于控件来执行某些操作。单选按钮用于在表单上提交信息,而不是用于切换可见性。这可能会让语音和屏幕阅读器用户感到困惑。
公开小部件传达其状态(展开或折叠)。复选框只标识它是否被选中。
JavaScript 对于传达状态是必要的,即使你可以在其他地方避免它,它也是 JS 的有效用法。
感谢这篇很棒的文章
我很好奇在哪里可以找到你提到的产品卡网格的 Pen
产品卡网格的 Storybook 截图来自工作中的私有仓库。
这是一个“之前”的截图,展示了我们现有的产品网格,在添加“快速查看”功能之前。
为了 Codepen 原型的目的,我在文章中链接了,我简化了“卡片”,以便重点关注 CSS 网格的行为,以及“切换”注入的快速查看卡片。
我知道这主要是关于使用网格的。但由于您的实际卡片(以及可能您的快速视图)包含图像,所以如果在示例中包含它们会很有趣。鉴于视觉卡片布局,图像在源代码顺序中放在哪里?
好问题。
我们的 Storybook 库中有许多“卡片”组件。
产品卡非常复杂,具有以下通用结构
图像在源代码顺序中排在第一位,但它们不是链接。
我对网格布局的可能性了解得越多,我就越感到惊讶,感谢您的演示。
我在 JavaScript 部分注意到,您没有为 data 属性 querySelector 关闭括号(第 2 行、42 行和 60 行)。
这只是一个错别字,还是我对此一无所知?
啊,说得好!
错别字……现在都修正了。:)
您好,
这种方法可以用于实现选项卡式界面,单击选项卡时打开下方相关的 内容吗?
屏幕需要初始打开一个选项卡。
在我看来,我认为选项卡式界面不适合这种 CSS 网格实现。
如果 JavaScript 被禁用或“损坏”,选项卡式界面应该显示所有原本隐藏的内容。我不确定如果布局由网格管理,你将如何处理这种情况?所有内容块是否会在网格中一个接一个地渲染?
其次,在较小的屏幕上,内容之后出现的选项卡会怎么样?它们会渲染在内容下方。这不是选项卡式界面应该工作的方式。
第二点引出了另一个有趣的问题……选项卡式界面应该如何在小屏幕上渲染?
我采用了 Hayden Pickering 在他的包容性组件中描述的相同解决方案。我把选项卡变成了手风琴。
感谢您并祝贺您撰写这篇精彩的文章,它回答了我一直在问自己的问题。
问题:是否可以通过单击 LI 来打开和关闭缩放,而无需使用按钮?
代码示例……
……
提前感谢
Pascal
谢谢。很高兴您发现这篇文章有用。
为了回答您的问题……
我不建议让
<li>
可点击以打开快速视图。您需要实现<button>
元素拥有的所有原生浏览器行为和可访问性。例如,键盘焦点,将spacebar
和enter
键绑定到触发操作,ARIArole="button"
等。网上有很多文章解释了为什么在类似情况下使用原生
<button>
始终是最佳方法。此外,在我的特定实现中,我有一个产品卡网格,其中每个卡已经包含多个
<a>
或<button>
元素(它们要么链接到产品页面,要么保存/分享产品)。因此,我不得不添加一个额外的<button>
来专门触发快速查看行为。提醒一下,如果将列表元素的 display 属性更改为 grid/flex,许多屏幕阅读器将不再遵守列表语义。
您需要将它们恢复,例如
role="list"
和role="listitem"
嗨 Grace……感谢您的评论。这在之前的评论中已经讨论过了——请查看https://css-tricks.org.cn/expandable-sections-within-a-css-grid/#comment-1783940
太棒了。我一直到处寻找一种方法将响应式网格与手风琴式布局结合起来,就像您做的那样,最后我找到了合适的搜索词组合。原来我少了一行——grid-auto-flow:dense。我甚至向 ChatGPT 寻求帮助,但它也无法做到!