不知道你在使用 CSS Flexbox 布局或者 CSS Grid 布局时,是否会碰到一些奇怪的问题,比如在 Firefox、Safari 和 Edge 浏览器中容器的滚动失效,再比如 Flex 项目或 Grid 项目会因其他内容拉伸变形。你可能会认为这一切都是 CSS Flexbox 或 Grid 布局的 Bug,孰不知,这一切都是它们的“特性”。
那么,在实际工作中,碰到这些现象我们应该如何解决,或者说如何在编码的时候就避免呢?这节课程中,我们一起来探讨这方面的问题。
布局中的滚动失效
在介绍布局滚动失效前,我们先来说一下使用场景吧。毕竟不是所有布局中的滚动都是会失效的,它只发生在一些特定的场景中,而且不是所有浏览器下都会发生。比如下图这样的布局效果,在移动端的布局上已经是非常常见的了:
上图也是 Web 布局中的十大经典布局之一,被称为百分百无滚动布局,即 body
自身不滚动,而且页面的页头和页脚分别固定在页面顶部和底部,只是中间的内容会因内容增加而发生滚动:
我曾在《使用 Flexbox 构建经典 Web 布局》和《使用 Grid 构建经典 Web 布局》中分别介绍了 CSS Flexbox 和 CSS Grid 是如何实现上图这种布局效果。我们简单回顾一下:
<body>
<header>Header</header>
<main>Main Content</main>
<footer>Footer Bar</footer>
</body>
body {
height: 100vh;
overflow: hidden;
}
/* CSS Flexbox Layout */
.flexbox {
display: flex;
flex-direction: column;
gap: 4px;
}
main {
flex: 1 1 0%;
min-height: 0;
}
/* CSS Grid Layout */
body {
display: grid;
gap: 4px;
grid-template-rows: min-content minmax(0, 1fr) min-content;
}
main {
overflow: hidden auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
}
就拿 Flexbox 布局来说吧,在滚动容器 main
上显式设置了 flex: 1 1 0%
和 min-height: 0
(避免 Flex 项目尺寸小于最小内容尺寸),并且显式在 main
上设置了 overflow-y
的值为 auto
(或 scroll
)。按理说,当 main
容器的内容高度大于 main
容器的高度时,它应该会出现垂直滚动条。但事与愿违,部分浏览器中 main
滚动失效(当时是在 iOS 系统中发现的这个 Bug):
Demo 地址:https://codepen.io/airen/full/GRXpgLP (可能只有个别浏览器中,滚动容器的滚动才失效)
为了防止滚动容器失效,我们需要对 HTML 结构做出相应的调整:
<!-- Flexbox Layout -->
<body class="flexbox"><!-- flex-direction: column -->
<header></header><!-- 固定高度 -->
<main>
<div class="overflow--container">
<div class="overflow--content">
<!-- 主内容放在这里 -->
</div><!-- 主内容 -->
</div><!-- 滚动容器 height: 100%; overflow-x: auto -->
</main><!-- 弹性尺寸:flex: 1 1 0%; min-height: 0 很重要 -->
<footer></footer><!-- 固定高度 -->
</body>
<!-- Grid Layout -->
<body class="grid"><!-- grid-template-rows: min-content minmax(0, 1fr) min-content -->
<header></header><!-- 固定高度 -->
<main>
<div class="overflow--container">
<div class="overflow--content">
<!-- 主内容放在这里 -->
</div><!-- 主内容 -->
</div><!-- 滚动容器 height: 100%; overflow-x: auto -->
</main><!-- 弹性尺寸:min-height: 0 很重要 -->
<footer></footer><!-- 固定高度 -->
</body>
对应的 CSS 样式规则如下:
/* CSS Flexbox Layout */
.flexbox {
height: 100%; /* 如果要等于视窗高度,请使用 100vh */
overflow: hidden; /* 这个很重要 */
display: flex;
flex-direction: column;
gap: 4px; /* 如果需要的话设置*/
}
main {
flex: 1 1 0%; /* 弹性 Flex 项目,占用 Flex容器的剩余空间 */
min-height: 0; /* 这个很重要,Flex 项目高度不小于内容最小高度 */
}
.overflow--container {
height: 100%; /* 滚动容器高度等于其父容器 */
overflow-y: auto; /* 内容高度大于容器高度时,出现垂直滚动条 */
}
/* CSS Grid Layout */
.grid {
height: 100%; /* 如果要等于视窗高度,请使用 100vh */
overflow: hidden; /* 这个很重要 */
display: grid;
grid-template-rows: min-content minmax(0, 1fr) min-content; /* minmax(0, 1fr) 替代 1fr,很重要 */
gap: 4px; /* 如果需要的话设置*/
}
main {
min-height: 0; /* 这个很重要,Grid 项目高度不小于内容最小高度,尤其是网格轨道没有设置 minmax(0, 1fr) */
}
.overflow--container {
height: 100%; /* 滚动容器高度等于其父容器 */
overflow-y: auto; /* 内容高度大于容器高度时,出现垂直滚动条 */
}
除了上面这种情景会让滚动失效之外,在 CSS Flexbox 布局中还有另外两种情形会致使滚动失效。先来看第一种,即 Flex 容器上设置 justify-content: end
致使容器水平滚动失效 。比如下面这个示例:
<div class="flex--container">
<Card />
<!-- 有很多个 Card 组件 -->
</div>
.flex--container {
display: flex;
justify-content: end;
}
造成这种现象的原因主要是和滚动条的设计有关。在不考虑书写模式(或阅读模式)之下,水平方向的内容是向容器右侧溢出,垂直方向的内容是向容器底部溢出。因此,在设计滚动条的时候就约定了,只有容器下方或右侧内容有多余,才需要滚动 。
如果这个滚动容器刚好是一个 Flex 容器,并且在 Flex 容器上显式设置了 justify-content
的值为 flex-end
,就会导致 Flex 容器的内容向左或向上溢出。这样就违背了滚动容器滚动条的设计规则,自然就无法触发容器的滚动条出现。简单地说,justify-content
取值 flex-end
会让内容反向溢出,致使滚动条失效 。
那么如何解决呢?熟悉 CSS Flexbox 布局的同学应该晓得,在 Flex 项目上设置 margin
的值为 auto
可以达到下图这样的效果:
你可能已经想到解决方案了。是的,可以在 Flex 项目上设置 margin-left
(或 margin-inline-start
)属性的值为 auto
,来达到在 Flex 容器上设置 justify-content: end
的等同效果 。在上面的示例中,我们只需要在第一张卡片 <Card />
上显式设置 margin-left
(或 margin-inline-start
)的值为 auto
即可。
.flex--container {
display: flex;
}
.flex--container .card:firt-child {
margin-left: auto;
/* 或者 */
margin-inline-start: auto;
}
如果 Flex 容器的 flex-direction
设置为 column
,那么就需要使用 margin-top
(或 margin-block-start
)来替代上面示例中的 margin-left
(或 margin-inline-start
):
/* 水平方向居右对齐 */
.flex--container--row {
display: flex;
}
.flex--container--row .card:first-child{
margin-left: auto;
/* 或者 */
margin-inline-start: auto;
}
/* 垂直方向靠底对齐 */
.flex--container--column {
display: flex;
flex-direction: column;
}
.flex--container--column {
margin-top: auto;
/* 或者 */
margin-block-start: auto;
}
再接着聊一下,Flexbox 布局中的另一种场景,滚动到容器边缘无法查看到全部内容。比如下面这个示例:
<div class="container">
<span>CSS</span>
<span>is</span>
<span>awesome!</span>
</div>
.container {
display: flex;
flex-direction: column;
align-items: center;
overflow-x: auto;
}
align-items
将所有 Flex 项目(即 span
元素)沿着侧轴水平居中对齐。当 Flex 容器 .container
有足够空间时,一切都完美,但如果容器没有足够多的空间来容纳 Flex 项目的内容时,就会出现“数据丢失”的情况:
由于 Flex 项目始终在 Flex 容器水平居中,Flex 项目宽度大于 Flex 容器宽度时,Flex 项目就会在左右两边溢出。问题是,左侧的溢出区域超出了 Flex 容器视口的起始边缘,你不能滚动到该区域 。
在未来,我们可以使用 CSS Flexbox 中的安全对齐方式来避免这种现象。即,在 align-items
属性值前添加 safe
关键词:
.container {
display: flex;
flex-direction: column;
align-items: safe center;
overflow-x: auto;
}
这样一来,Flex 项目对齐方式会切换到 start
(或 flex-start
) 模式,不会强制 Flex 项目居中对齐。
不幸的是,到目前为止,safe center
双值语法还没有得到主流浏览器的支持(仅得到 Firefox 浏览器的支持)。如果你在现在的业务中碰到类似的情景,又希望用户可以正常浏览到所有数据,你还可以像上一个示例那样,在 Flex 项目上设置 margin
的值为 auto
:
.container > span {
margin-inline: auto;
}
布局中的拉伸和挤压
不知道大家平时使用 CSS Flexbox 或 Grid 构建 Web 布局时,是否有碰到过元素的 UI 形状因为布局被拉伸或挤压,使 UI 变得难看。比如下图这个拉伸的效果:
就上图而言,不管是使用 CSS Flexbox 还是 Grid 来构建布局,都将是件轻而易举的事情。
<div class="card">
<img src="card-thumnail.jpg" alt="Card Thumnail" />
<p>Card Description</p>
<button>Button</button>
</div>
/* CSS Flexbox Layout */
.card {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card img,
.card button {
flex-shrink: 0;
}
.card p {
flex: 1 1 0%;
min-width: 0;
}
/* CSS Grid Layout */
.card {
display: grid;
grid-template-columns: min-content minmax(0, 1fr) min-content;
gap: 10px;
}
你可能已经发现了,不管是 Flexbox 布局还是 Grid 布局,个别项目(Flex 项目或 Grid 项目)会因为比自身更高的项目(比如示例中的段落 p
)而拉伸,和其他项目保持一样的高度。对于很多 Web 开发者来说,往往会忽略这一点。
对 Flexbox 或 Grid 布局对齐方式有了解的同学都知道,这是 Flexbox 和 Grid 布局中的正常现象。就拿 CSS Flexbox 来说吧,默认情况之下,所有 Flex 项目在 Flex 容器侧轴方向是默认拉伸的,它们的高度会和 Flex 容器侧轴尺寸相等,简单地说,就是 align-items
的默认值是 stretch
。
知道其原理之后,要避免这个现象就很简单了,只需要将 align-items
属性的值设置为非 stretch
值,比如 flex-start
、center
等。对于 CSS Grid 布局而言,也是同理:
.card--flexbox {
align-items: flex-start;
}
.card--grid {
align-items: start;
}
除此之外,你还可以在单个项目上设置 align-self
的值,比如:
.card--flexbox > *:not(p) {
align-self: flex-start;
}
.card--grid > *:not(p) {
align-self: start;
}
所以,大家在使用 Flexbox 或 Grid 构建 Web 布局时,应该尽可能地避免 align-items
的默认现象导致的 UI 拉伸。在有多行时,也需要考虑 align-content
属性。具体应该根据 Flexbox 和 Grid 布局对齐方式来做出正确的选择,从而避免 UI 的拉伸变形:
CSS Flexbox 除了会拉伸 UI 形状之外,有些场景还会对 UI 进行挤压。比如下面这个示例:
<div class="card">
<img src="thumnail.pgn" alt="Card Thumnail" />
<p>Card Description</p>
<div class="card__action">
<svg></svg>
</div>
</div>
.card {
display: flex;
align-items: center;
gap: 10px;
}
.card img {
display: block;
width: 4em;
aspect-ratio: 1;
}
.card__action {
display: inline-flex;
width: 3em;
aspect-ratio: 1;
}
.card p {
flex: 1 1 0%;
white-space: nowrap;
}
上面这种现象也是 Flexbox 的正常现象。造成这个现象,是由于段落 p
(它也是一个Flex项目)内容过长(设置了 white-space
或有长字符串),Flex 容器无剩余空间来放置它,这个时候将会对同一轴上的其他 Flex 项目进行挤压。大家知道,Flex项目的 flex
的默认值为:
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
flex-shrink
的值为1
,表示 Flex 项目可以被收缩。解决这种现象,我们有三种方法,最简单的方法是在段落 p
(Flex项目)元素上显式设置 min-width
的值为 0
:
.card p {
flex: 1 1 0%;
min-width: 0;
}
第二种方法,那就是在段落 p
元素显式设置 overflow
属性的值为非 visible
值,比如 hidden
。一般会和 text-overflow: ellipsis
结合起来使用:
.card p {
flex: 1 1 0%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
其中原委,在上一节课《Flexbox 和 Grid 中最小内容尺寸》中有详细介绍,这里就不再做重复性的阐述。最后一种是在不需要被挤压的 Flex 项目上显式设置 flex-shrink: 0
,这样做的好处是,它会告诉浏览器,不能因 Flex 容器不足来挤压我的空间:
.card > *:not(p) {
flex-shrink: 0;
}
其原理涉及到了 CSS Flexbox 中的 flex
属性,相对来说是比较复杂的一部分,理解起来更为吃力。有关于 flex
属性的阐述已超出这节课的范畴,如果你感兴趣或想深入了解 flex
属性相关的原理,那建议你移步阅读:
- Flexbox 布局中的 flex 属性的基础运用
- Flexbox 布局中的计算:通过扩展因子比例来扩展 Flex 项目
- Flexbox 布局中的计算:通过收缩因子比例来收缩 Flex 项目
- Flexbox 布局中的 flex-basis :谁能决定 Flex 项目的大小?
小结
这节课,我们通过几个简单的示例,向大家展示了 CSS Flexbox 和 Grid 布局时不为人知的一面。其中很多是很基础的知识点,只是很多 Web 开发者容易忽略,才致使自己构建的 Web UI 不完美,甚至用户无法访问到页面中的部分信息。也正如课程中所说,只需要简单的一行代码,就可以让你的 Web 页面变得更完善,比如:
- 使用 Flexbox 或 Grid 布局时,应该在 Flex 容器或 Grid 容器上显式重置
align-items
的默认值,避免 UI 被拉伸变形; - 在具备伸缩性的 Flex 项目上显式设置
min-width: 0
,或在不需要收缩的项目上显式设置flex-shrink:0
,避免 UI 被掠夺变形; - 在设置对齐方式时,应该添加
safe
关键词来避免数据无法全部展示; - 如果 Flex 容器同时也是一个滚动容器时,我们应该避开使用
justify-content: flex-end
,即使需要向右对齐(或靠底部对齐),应该尽可能地使用margin-left: auto
(或margin-top
)来替代justify-content: flex-end
。
它们都很简单,并没有你想象的那么复杂,请记住它们的使用姿势,在关键时刻能帮助你解决大问题。