Skip to content

不知道你在使用 CSS Flexbox 布局或者 CSS Grid 布局时,是否会碰到一些奇怪的问题,比如在 Firefox、Safari 和 Edge 浏览器中容器的滚动失效,再比如 Flex 项目或 Grid 项目会因其他内容拉伸变形。你可能会认为这一切都是 CSS Flexbox 或 Grid 布局的 Bug,孰不知,这一切都是它们的“特性”。

那么,在实际工作中,碰到这些现象我们应该如何解决,或者说如何在编码的时候就避免呢?这节课程中,我们一起来探讨这方面的问题。

布局中的滚动失效

在介绍布局滚动失效前,我们先来说一下使用场景吧。毕竟不是所有布局中的滚动都是会失效的,它只发生在一些特定的场景中,而且不是所有浏览器下都会发生。比如下图这样的布局效果,在移动端的布局上已经是非常常见的了:

img

上图也是 Web 布局中的十大经典布局之一,被称为百分百无滚动布局,即 body 自身不滚动,而且页面的页头和页脚分别固定在页面顶部和底部,只是中间的内容会因内容增加而发生滚动:

img

我曾在《使用 Flexbox 构建经典 Web 布局》和《使用 Grid 构建经典 Web 布局》中分别介绍了 CSS Flexbox 和 CSS Grid 是如何实现上图这种布局效果。我们简单回顾一下:

HTML
<body>
    <header>Header</header>
    <main>Main Content</main>
    <footer>Footer Bar</footer>
</body>
CSS
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):

img

Demo 地址:https://codepen.io/airen/full/GRXpgLP (可能只有个别浏览器中,滚动容器的滚动才失效)

为了防止滚动容器失效,我们需要对 HTML 结构做出相应的调整:

img

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
/* 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;  /* 内容高度大于容器高度时,出现垂直滚动条 */
}

img

Demo 地址:https://codepen.io/airen/full/LYJpZjE

除了上面这种情景会让滚动失效之外,在 CSS Flexbox 布局中还有另外两种情形会致使滚动失效。先来看第一种,即 Flex 容器上设置 justify-content: end 致使容器水平滚动失效 。比如下面这个示例:

HTML
<div class="flex--container">
    <Card />
    <!-- 有很多个 Card 组件 -->
</div>
.flex--container {
    display: flex;
    justify-content: end;
}

img

Demo 地址: https://codepen.io/airen/full/RwYWmKE

造成这种现象的原因主要是和滚动条的设计有关。在不考虑书写模式(或阅读模式)之下,水平方向的内容是向容器右侧溢出,垂直方向的内容是向容器底部溢出。因此,在设计滚动条的时候就约定了,只有容器下方或右侧内容有多余,才需要滚动

如果这个滚动容器刚好是一个 Flex 容器,并且在 Flex 容器上显式设置了 justify-content 的值为 flex-end ,就会导致 Flex 容器的内容向左或向上溢出。这样就违背了滚动容器滚动条的设计规则,自然就无法触发容器的滚动条出现。简单地说,justify-content 取值 flex-end 会让内容反向溢出,致使滚动条失效

那么如何解决呢?熟悉 CSS Flexbox 布局的同学应该晓得,在 Flex 项目上设置 margin 的值为 auto 可以达到下图这样的效果:

img

你可能已经想到解决方案了。是的,可以在 Flex 项目上设置 margin-left(或 margin-inline-start)属性的值为 auto ,来达到在 Flex 容器上设置 justify-content: end 的等同效果 。在上面的示例中,我们只需要在第一张卡片 <Card /> 上显式设置 margin-left (或 margin-inline-start)的值为 auto 即可。

CSS
.flex--container {
    display: flex;
}

.flex--container .card:firt-child {
    margin-left: auto;
    
    /* 或者 */
    margin-inline-start: auto;
}

Demo 地址:https://codepen.io/airen/full/BaOogjL

如果 Flex 容器的 flex-direction 设置为 column ,那么就需要使用 margin-top (或 margin-block-start)来替代上面示例中的 margin-left (或 margin-inline-start):

CSS
/* 水平方向居右对齐 */
.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;
}

img

Demo 地址: https://codepen.io/airen/full/WNgQqoO

再接着聊一下,Flexbox 布局中的另一种场景,滚动到容器边缘无法查看到全部内容。比如下面这个示例:

HTML
<div class="container">
    <span>CSS</span>
    <span>is</span>
    <span>awesome!</span>
</div>
CSS
.container {
    display: flex;
    flex-direction: column;
    align-items: center;
    overflow-x: auto;
}

align-items 将所有 Flex 项目(即 span 元素)沿着侧轴水平居中对齐。当 Flex 容器 .container 有足够空间时,一切都完美,但如果容器没有足够多的空间来容纳 Flex 项目的内容时,就会出现“数据丢失”的情况:

img

由于 Flex 项目始终在 Flex 容器水平居中,Flex 项目宽度大于 Flex 容器宽度时,Flex 项目就会在左右两边溢出。问题是,左侧的溢出区域超出了 Flex 容器视口的起始边缘,你不能滚动到该区域 。

在未来,我们可以使用 CSS Flexbox 中的安全对齐方式来避免这种现象。即,在 align-items 属性值前添加 safe 关键词:

CSS
.container {
    display: flex;
    flex-direction: column;
    align-items: safe center;
    overflow-x: auto;
}

这样一来,Flex 项目对齐方式会切换到 start (或 flex-start) 模式,不会强制 Flex 项目居中对齐。

img

不幸的是,到目前为止,safe center 双值语法还没有得到主流浏览器的支持(仅得到 Firefox 浏览器的支持)。如果你在现在的业务中碰到类似的情景,又希望用户可以正常浏览到所有数据,你还可以像上一个示例那样,在 Flex 项目上设置 margin 的值为 auto

CSS
.container > span {
  margin-inline: auto;
}

img

Demo 地址:https://codepen.io/airen/full/zYJrOxZ

布局中的拉伸和挤压

不知道大家平时使用 CSS Flexbox 或 Grid 构建 Web 布局时,是否有碰到过元素的 UI 形状因为布局被拉伸或挤压,使 UI 变得难看。比如下图这个拉伸的效果:

img

就上图而言,不管是使用 CSS Flexbox 还是 Grid 来构建布局,都将是件轻而易举的事情。

HTML
<div class="card">
    <img src="card-thumnail.jpg" alt="Card Thumnail" />
    <p>Card Description</p>
    <button>Button</button>
</div>
CSS
/* 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;
}

img

Demo 地址:https://codepen.io/airen/full/wvEMwab

你可能已经发现了,不管是 Flexbox 布局还是 Grid 布局,个别项目(Flex 项目或 Grid 项目)会因为比自身更高的项目(比如示例中的段落 p )而拉伸,和其他项目保持一样的高度。对于很多 Web 开发者来说,往往会忽略这一点。

FlexboxGrid 布局对齐方式有了解的同学都知道,这是 Flexbox 和 Grid 布局中的正常现象。就拿 CSS Flexbox 来说吧,默认情况之下,所有 Flex 项目在 Flex 容器侧轴方向是默认拉伸的,它们的高度会和 Flex 容器侧轴尺寸相等,简单地说,就是 align-items 的默认值是 stretch

知道其原理之后,要避免这个现象就很简单了,只需要将 align-items 属性的值设置为非 stretch 值,比如 flex-startcenter 等。对于 CSS Grid 布局而言,也是同理:

CSS
.card--flexbox {
    align-items: flex-start;
}

.card--grid {
    align-items: start;
}

img

Demo 地址:https://codepen.io/airen/full/bGxEGwq

除此之外,你还可以在单个项目上设置 align-self 的值,比如:

CSS
.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 的拉伸变形:

img

CSS Flexbox 除了会拉伸 UI 形状之外,有些场景还会对 UI 进行挤压。比如下面这个示例:

HTML
<div class="card">
    <img src="thumnail.pgn" alt="Card Thumnail" />
    <p>Card Description</p>
    <div class="card__action">
        <svg></svg>
    </div>
</div>
CSS
.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;
}

img

Demo 地址:https://codepen.io/airen/full/NWLxWMw

上面这种现象也是 Flexbox 的正常现象。造成这个现象,是由于段落 p(它也是一个Flex项目)内容过长(设置了 white-space 或有长字符串),Flex 容器无剩余空间来放置它,这个时候将会对同一轴上的其他 Flex 项目进行挤压。大家知道,Flex项目的 flex 的默认值为:

CSS
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;

flex-shrink的值为1,表示 Flex 项目可以被收缩。解决这种现象,我们有三种方法,最简单的方法是在段落 p (Flex项目)元素上显式设置 min-width 的值为 0

CSS
.card p {
    flex: 1 1 0%;
    min-width: 0;
}

img

Demo 地址:https://codepen.io/airen/full/OJoMPRv

第二种方法,那就是在段落 p 元素显式设置 overflow 属性的值为非 visible 值,比如 hidden 。一般会和 text-overflow: ellipsis 结合起来使用:

CSS
.card p {
    flex: 1 1 0%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

img

Demo 地址:https://codepen.io/airen/full/jOvWEmd

其中原委,在上一节课《Flexbox 和 Grid 中最小内容尺寸》中有详细介绍,这里就不再做重复性的阐述。最后一种是在不需要被挤压的 Flex 项目上显式设置 flex-shrink: 0 ,这样做的好处是,它会告诉浏览器,不能因 Flex 容器不足来挤压我的空间:

CSS
.card > *:not(p) {
    flex-shrink: 0;
}

img

Demo 地址:https://codepen.io/airen/full/oNPbgpW

其原理涉及到了 CSS Flexbox 中的 flex 属性,相对来说是比较复杂的一部分,理解起来更为吃力。有关于 flex 属性的阐述已超出这节课的范畴,如果你感兴趣或想深入了解 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

它们都很简单,并没有你想象的那么复杂,请记住它们的使用姿势,在关键时刻能帮助你解决大问题。