图解CSS:CSS溢出(Part2)

发布于 大漠

第一部分主要和大家聊了CSS溢出的概念和理论相关的,在第一部分主要以实例,问题排查和常见溢出问题三个部分展开。

溢出实例

在构建 Web 页面或应用的时候,总是避免不了给容器设置一个具有约束性的尺寸,有的时候,因为容器没有足够的空间来容纳内容,从而造成内容溢出容器,严重的会打破 Web 布局,干扰页面的美观。为了让这些现象能不干扰 Web 布局,所以需要CSS溢出特性。除此之外,我们在构建一些Web组件和实现一些UI效果,也需要依赖CSS溢出特性。那么接下来,我们来看一些溢出相关的案例。

水平滚动的容器

在 Web 布局中,水平滚动是一种常见的布局做法,因为它有助于减少屏幕较小的设备的垂直空间。比如在 H5 的页面中,像Swiper这样的幻灯片组件或类似这种水平滑动的UI效果随处可见:

比如上面视频中的卡片和导航菜单都具有水平滚动的效果,我们可以通过水平剪切内容并允许其滚动来创建一个水平滚动容器。在 CSS 中要实现该效果,需要具备两个条件:

  • 滚动容器的宽度 width(或 inline-size)具有一定的约束,它有一个固定的值,哪怕 width的值是根据其父元素(或祖先元素)的width计算得到的
  • 滚动容器显式设置 overflow-x 的值为 scrollauto,建议将其设置为 auto,因为 scroll 值会让容器的滚动条一直呈现

我们来看一个卡片在水平滚动容器中滑动的效果:

<!-- HTML -->
<ul class="cards">
    <li class="card"></li>
    <!-- ... -->
    <li class="card"></li>
</ul>

/* CSS */
.cards {
    overflow-x: auto;

    /* 滚动捕捉*/
    scroll-snap-type: x mandatory;

    /* 不显示滚动条 */
    scrollbar-width: 0;
}

.card {
    scroll-snap-align: center;
}

/* Flexbox Layout*/
.cards--flex {
    display: flex;
    gap: calc(var(--gutter) / 2);
    flex-wrap: nowrap;
}

.cards--flex .card {
    flex: 1 0 calc(50% - var(--gutter) * 2);
}

/* Grid Layout */
.cards--grid {
    display: grid;
    gap: calc(var(--gutter) / 2);
    grid-template-columns: 10px;
    grid-auto-flow: column;
    grid-auto-columns: calc(50% - var(--gutter) * 2);
    align-items: stretch;
}

最终效果如下:

就这个示例而言,滚动容器.cards的宽度和父容器的宽度相同(width: 100%),不管我们使用的是Flexbox还是Grid布局,都让所有卡片.card保持在一行排列,所有卡片宽度加上卡片之间的间距就大于容器宽度,卡片就会溢出容器:

再来看另一个有关于水平滚动容器的示例,即水平滚动导航栏:

我们可以使用同样的方法来实现,具体代码请查看下面的示例:

水平滚动容器不管是Flexbox还是Grid布局,如果显式设置了justify-content的值为flex-endend,水平滚动将会失效。具体的解决方案将放到后面来阐述!

模态框(弹窗)

模态框(Modal)是Web中很常见的一个 Web 组件,大多数情况之下,模态框具有一个固定的宽高尺寸,至少会有一个最大高度(max-height)的限制避免模态框顶部和底部被裁剪(模态框一般会使用绝对定位,让其位于页面的中间位置)。对于一个具有尺寸约束的模态框而言,当模态框内容太长时,我们可以很容易地使该区域可滚动:

我们可以使用 HTML5 的 <dialog> 元素来构建模态框:

<!-- HTML -->
<dialog>
    <div class="dialog__content">
        <div class="dialog__heading"></div>
        <div class="dialog__body"></div>
        <div class="dialog__footer"></div>
    </div>
</dialog>

.dialog__content 设置一个max-widthmax-height值,用来约束模态框的尺寸。另外,使用CSS Grid来布局,让.dialog__body 的高度为 1fr

.dialog__content {
    display: grid;
    grid-template-columns: auto;
    grid-template-rows: auto 1fr auto;
    grid-template-areas: 
        "heading"
        "body"
        "footer";
    max-width: 50vw;
    max-height: 50vh;
}

.dialog__heading {
    grid-area: heading;
}

.dialog__body {
    grid-area: body;
}

.dialog__footer {
    grid-area: footer;
}

在这种情况之下,我们并不知道放置在 .dialog__body 中的内容是什么,所占空间是多少,有可能整个容器无法容纳所放置的内容。在这种情况之下,我们需要在.dialog__body 容器上设置overflow-y的值为auto。这样一来,当内容高度超过容器高度时,就会出现滚动条:

如果.dialog__body出现滚动条时,就会产生多个容器出现滚动条(比如.dialog__bodybody两个容器元素),即 z轴不同层的滚动容器。此时,模态框向下滚动到底部时,如果继续往下滚动则会引起弹框下面的内容,比如body元素会继续滚动。这也是滚动链默认的表现:

为了避免滚动扩散到其他滚动容器,我们可以在顶部的滚动容器中(即 .dialog__body)设置overscroll-behavior属性设置为 contain。这样一来,模态框中的滚动容器滚动到底部之后也不会影响模态框底部的滚动容器(body)的滚动:

关键代码:

.dialog__body {
    overflow-y: auto; 
    overscroll-behavior: contain;
    min-height: 0
}

最终效果如下:

注意,模态框的布局了可以使用 CSS Flexbox 来布局,.dialog__body可以显式设置flex:1,让其占用Flexbox容器的剩余空间,只不过这种布局方案会在个别系统中(比如iOS系统)中让overflow-y失效。这是触发了Flex项目的边缘情况所产生的Bug,具体解决方案稍后在“溢出常见问题与排查”一节中介绍。

带有圆角的卡片

带圆角的卡片是常见的一种UI设计,不少开发者还原带有圆角的卡片UI时,更倾向于给卡片容器的子元素设置border-radius,比如:

<!-- HTML -->
<div class="card">
    <img class="card__thumb" src="" alt="" />
    <div class="card__content"></div>
</div>

/* CSS */
.card__thumb {
    border-radius: 10px 10px 0 0;
}

.card__content {
    border-radius: 0 0 10px 10px;
}

但我们面对的卡片UI可能是多样化的,比如:

面对上图这样的UI设计,如果按上面代码来处理卡片圆角效果的话,工作量相对而言是更多的。在这种情况之下,更好的处理方式是在卡片容器.card设置border-radius,并且在该容器上显式设置 overflow 的值为 hidden。这样一来,不管卡片是如何布局,圆角的设置相对而言都会更灵活一些:

.card {
    border-radius: 10px;
    overflow: hidden;
}

在容器显式设置 overflow: hidden 可以避免卡片子元素与容器上下顶角处溢出的内容在视觉上看上去被裁切掉:

文本截断

前面提到过,文本截断主要有“单行文本截断” 和 “多行文本截断” 两种类型。需要注意的是,这里所说的文本截断并不是单纯的将溢出的文本裁剪掉,而是截剪后的文本末尾添加省略号指示符(...),以表示还有更多的文本内容(如上图所示)。

在 CSS 中实现单行文本截断和多行文本截断技术手段各有差异:

单行文本截断常用 text-overflow: ellipsis 结合 white-space: nowrapoverflow: hidden

.single-line-truncation {
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
}

多行文本截断常用 line-clamp,但该属性必须使用 display: -webkit-box-webkit-box-orient: vertical 才能生效:

.multiple-line-truncation {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; /* 截断的最大行数 */
    overflow: hidden;
}

如果将line-clamp的值设置为 1 的时候,那么它的效果有点类似于 text-overflow: ellipsiswhite-space: nowrapoverflow: hidden 效果等同。不过,使用 line-clamp 来实现文本截断效果时,不能和 white-space: nowrap 一起使用,而且不能显式在容器上设置 padding-top(或padding-bottom),也不能显式设置具体的height值。

我们来看一个简单的实例,使用 text-overflow: ellipsiswhite-space: nowrapoverflow: hidden 实现的单行文本截断的效果:

列表项标题较长(文本过多)时,会被截断,并且提供省略号指示符(...),反之则不会。

在这个示例中,列表项采用的是 Flexbox 布局,在这种情况之下,常常在需要截断文本的容器(它是一个Flex项目)上显式设置 min-width: 0,用来规避一些Flexbox布局引起的Bug:

.flex-item {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

这个同样适用于网格轨道尺寸 1fr 的网格布局上:

动效

在使用 CSS 的 transitionanimation 做一些动画效果时会离不开 overflow: hidden。比如下面这样的效果:

上面的动画效果借助了CSS的伪元素::before::after新增移动的元素,如果没有在容器上显式设置 overflow:hidden的话,这些动画元素会溢出容器:

也就是说,当涉及到动画时,overflow: hidden的好处在于剪切隐藏的元素,可以在悬停时显示。

下面这几行代码是示例中按钮效果的:

.button {
    position: relative;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    color: #fff;
    background: linear-gradient(to bottom, #00acee, #0072e0);
    font-size: 1.2rem;
    font-weight: 500;
    padding: 0.5em 1.5em;
    border-radius: 999rem;
    text-decoration: none;
    transition: 0.3s ease-out;
    background-origin: border-box;
    cursor: pointer;
    text-shadow: 1px 1px 2px rgb(0 0 0 / 40%);
    transform: translateZ(0);
}

.button:hover {
    box-shadow: 0 0 0 6px rgb(0 172 238 / 15%);
}

.button::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background: #000;
    opacity: 0.25;
    border-radius: 999rem;
    z-index: -1;
    transition: 0.2s ease-in;
}

.button:nth-child(1)::before {
    transform: translateX(-100%);
}

.button:nth-child(2)::before {
    transform: translateX(100%);
}

.button:nth-child(3)::before {
    transform: translateY(100%);
}

.button:nth-child(4)::before {
    transform: scale(0);
}

.button:nth-child(1):hover::before {
    transform: translateX(0);
}

.button:nth-child(2):hover::before {
    transform: translateX(0);
}

.button:nth-child(3):hover::before {
    transform: translateY(0);
}

.button:nth-child(4):hover::before {
    transform: scale(1);
}

:root {
    --overflow: hidden;
}

.button {
    overflow: var(--overflow);
}

溢出常见问题与排查

在 Web 开发过程中,overflowtext-overflowline-clamp 等属性的使用还是很常见。通过前面的介绍,我们对 CSS 的溢出模块有了一个较清晰的认识,也清楚如何使用溢出模块中的相关属性,但在实际使用过程中,总是会碰到一些离奇的现象,让人琢磨不透。比如下面这样的一个示例,使用空元素来绘制图形(模拟一张背景氛围图)。按照常里,我们在容器上显式设置了:

.root {
    width: 100vw; 
    min-height: 100vh;
    overflow-x: hidden;
}

正常的理解,容器不会出现水平滚动条,但事实却并非如此:

正如你所看到的,虽然显式设置了overflow-x: hidden,水平方向还是会出现滚动条,溢出的内容并没有被裁切。对于这样的现象,往往喜欢说成使用overflow碰到的坑或者说无法正常工作。那么在实际使用的过程中,常常会碰到一些什么样的坑呢?碰到这些所谓的坑,有时候能很快的定位到问题所在,并且解决掉,不过有些坑,需要一些调试的技巧。

接下一来,我们将探讨溢出问题的原因以及如何解决这些问题。我们还将探讨开发者工具中的现代功能如何使修复和调试的过程更加容易。

什么是溢出问题?

在讨论溢出问题之前,我们应该先弄清楚什么是溢出问题。从前面的内容中我们可以得知,当Web上无意中出现一个水平滚动条,允许用户水平滚动时,就会发生溢出问题。这可能是由不同的因素造成的。

它的发生可能是因为内容出乎意料的宽,或者固定宽度的元素比视口宽。我们将在接下来的内容中探讨所有的原因。

如何发现溢出

解决溢出问题最关键的地方是要先注意到它。如果我们知道溢出发生的时间和地点,就可以对Web上的溢出容器(元素)进行监控,从而更好的发现和排查溢出问题。在开发过程中,可以使用不同的方法来检测溢出,包括手动向左或向右滚动或使用 JavaScript 脚本。

我们先来探讨一下检测溢出的方法。

向左或向右滚动

检测溢出最简单的,最粗暴的方式就是手动向左或向右滚动,如果能滚动,就说明页面或页面中的某个容器已经有内容溢出,说明已经出现问题了(除非你设计的时候就需要有滚动效果)。

使用JavaScript来检测溢出

开发者可以在浏览器调试工具中添加JavaScript脚本,把Web页面中比 body 元素更宽的元素都打印出来:

var docWidth = document.documentElement.offsetWidth;

[].forEach.call(
    document.querySelectorAll('*'),
    function(el) {
        if (el.offsetWidth > docWidth) {
            console.log(el);
        }
    }
);

上面的示例脚本有一定的局限性,只取出页面所有元素的宽度和body的宽度比较,对于子元素和其父元素宽度相比较就不适用了,而且也检测不到伪元素::before::after的宽度。

CSS 检测法

就我个人而言,以往在排查 CSS 的问题时,总是喜欢在有问题的元素上添加一个 outline

element {
    outline: 1px solid yellow;
}

这个方法同样可以用来检测溢出。简单地说,可以使用CSS的通配符选择器*,在页面上所有元素上添加outline,来查看最终效果:

*,
*::before,
*::after {
    outline: 1px solid red;
}

也可以使用 @Addy OsmaniCSS布局调试器可视化你的CSS布局。用随机的(有效的)CSS十六进制颜色勾勒出页面上的每个DOM元素。

你只需要把下面这段脚本放到你的页面中或者在浏览器调试器中输入:

[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})

你可以看到每个DOM元素都有一个随机生成具有不同颜色的outline

排除法

排除法相对而言是一个比较“笨”的方法,但这个方法在某些情况下是特别实用的。简单地说,就是打开浏览器的开发者调试工具,从<body>的第一个子元素开始,一个一个地删除元素。一旦问题消失,那么你刚刚删除的部分可能就是原因所在。

排除法特别适用于你已经发现问题所在,但不知道为什么会发生的情况下很有用。

在实际开发中,你可以根据自己具体的场景选择不同的方法来检测溢出。一旦你找到了溢出发生的位置,那么为进一步的调试或排除问题就会变得容易地多。

常见的溢出问题

现在我们知道可以用哪些方法来检测溢出问题了,接下来再来看看一些常见的溢出溢出问题以及如何修复这些溢出问题。

固定宽度引起溢出问题

造成溢出的最常见原因就是元素的width设置了一个固定值。

.element {
    width: 368px;
}

如果使用固定宽度会引起问题,一般来说,应该尽量避免使用。即使在开发的过程中真的有必要显式设置width值,最好也是配合max-width一起使用。如此一来,有足够空间时,会使用max-width设置元素宽度,从而达到你预设的宽度,反之空间不够时,会根据父容器的宽度来计算。

.element {
    width: 100%;
    max-width: 368px;
}

也可以像下面这样来设置元素的宽度:

.element {
    width: 368px;
    max-width: 100%;
}

这两段代码略有细微的差异:

  • 第一段代码,当元素计算出来的宽度大于 max-width 设置的值时,将会以max-width的值为最终计算值
  • 第二段代码,当max-width计算出来的值小于 width 设置的值时,将会以 max-width的值为最终计算值

不管哪段代码,最终都是元素的 widthmax-width 值来做比较,

CSS 中的属性取值为百分比(%)时,不同属性计算的参考物是不一样的,比如这里所说的宽度width取值为%时,是相对于其父容器的width来计算,有关于这方面更详细的介绍可以阅读《CSS中百分比单位计算方式》一文。

另外,widthmin-widthmax-width同时出现在一个元素上时,将会按下面的规则来决定元素的宽度

  • 元素的 width 大于或等于 max-width 时,max-width 会覆盖 width
  • 元素的 width 小于或等于 min-width 时,min-width 会覆盖 width
  • min-width 大于 max-width 时,min-width 优先级将高于 max-width

当然,也可以使用 CSS 的比较函数 min() 来设置,比如上面的代码,可以换成下面这样的:

.element {
    width: min(100%, 368px);
}

使用min()函数设置最大值

来看一个具体的示例:

正如上面示例所示,这种方式也适合用于<img>元素上:

在 Web 开发的过程中,<img> 的宽度会大于(也有可能小于)其容器的宽度,往往开发者更多的期望是让图片能自动响应其容器的宽度,为了达到这种效果,开发者往往使用 max-width: 100% 来设置 img 的尺寸:

img {
    max-width: 100%;
}

更多开发者喜欢把这段代码写在重置CSS的文件中,比如 reset.css 中:

img {
    display: block;
    max-width: 100%;
    height: auto;
}

说个题外话,就响应式图片而言,这种方式相对而言是简单粗暴的,现代Web开发当中,应该使用一些新特性,比如 imgsrcsetsizes的结合,或者使用 <picture><source> 来实现。如果你对这方面感兴趣的话,可以阅读 《响应式图片使用指南:Part1Part2Part3》。

未使用 flex-wrap 的 Flexbox 布局

时至今日,Flexbox 已成为主流的布局技术之一(如果你对 Flexbox 还不怎么了解,建议你花点时间阅读 图解CSS 系列中的 《 Flexbox布局Part1Part2》)。 虽然 Flexbox 用于布局非常灵活,但它也很容易造成溢出问题。

当一个 Flexbox 容器中有多个 Flex 项目时,如果未在 Flexbox 容器上显式设置 flex-wrapwrap 时(flex-wrap的默认值是nowrap,不管Flexbox容器是否有足够的空间,Flex项目都不换行)以及未对 Flex 项目做伸缩处理时,在Flexbox容器空间不足时,会造成Flex项目溢出Flexbox容器。

避免这种现象出现的最佳方式是在Flexbox容器中显式设置 flex-wrap的值为 wrap(除非你不希望断行出现):

.flex__container {
    display: flex;
    flex-wrap: wrap;
}

这样编写的CSS代码是更具健壮性,也是在编写防御式的CSS。

如果你对如何编写防御式CSS感兴趣的话,可以移步阅读《如何编写防御式的 CSS》一文,或 @Ahmad Shadeed 的 《Defensive CSS》!

网格布局引起的溢出

CSS Grid 布局 是目前唯一的一种二维布局技术,在布局上他比 Flexbox 布局更灵活,同样也要复杂的多。如果要掌握好 CSS Grid 布局技术,还是需要花费不少时间的。在这里我们不花时间专门讨论 CSS Grid 布局技术,如果你想学习的话,可以移步阅读《2022年不能再错过 CSS 网格布局了》一文。在这里我们主要来聊聊网格布局引起的溢出问题。

如果你要构建一个水平居中的布局效果,在 CSS Grid 中,我们可以像下面这样来构建:

body {
    display: grid;
    grid-template-columns: 1fr 75ch 1fr;
    gap: 1rem;
}

.container {
    grid-area: 1 / 2 / -1 / 3;
}

它有点类似于:

body {
    display: flex;
    justify-content: center;
}

.container {
    width: 75ch;
    margin-left: 1rem;
    margin-right: 1rem;
}

不难发现,当浏览器视窗的宽度大于 75ch + 2rem 时,.container 水平居中,一切都正常。不过,当浏览器视窗的宽度小于 75ch + 2rem 时,.container 会溢出,页面在浏览器视窗中能左右滚动:

在 CSS 中有多种方式可以避免这种现象,最常见的一种方式是使用 CSS 的媒体查询 @media,只有在足够多的空间下才设置多列网格。我们可以像下面这样来调整代码:

body {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
}

@media (min-width: 812px) {
    body {
        grid-template-columns: 1fr 75ch 1fr;
    }

    .container {
        grid-area: 1 / 2 / -1 / 3;
    }
}

如果你对 CSS Grid 熟悉的话,这里还可以使用 minmax() 函数来设置网格轨道尺寸,可以避免 CSS 媒体查询的使用:

body {
    display: grid;
    grid-template-columns: 1fr minmax(calc(320px - 2rem), 75ch) 1fr;
    gap: 1rem;
}

.container {
    grid-area: 1 / 2 / -1 / 3;
}

示例中的 320px 是浏览器最小视窗值的约束。你可以在代码中使用 CSS 的自定义属性来对视窗"最大值"、“最小值”和“网格间距”进行定义:

body {
    --limit-container-width: 75ch; /* 最大宽度 */
    --min-viewport-width: 320px; /* 视窗最小宽度 */
    --gutter: 1rem; /* 网格间距 */
    display: grid; 
    grid-template-columns: 
        1fr 
        minmax( calc(var(--min-viewport-width) - var(--gutter) * 2), var(--limit-container-width) ) 
        1fr; 
    gap: var(--gutter);
}

你可能已经想到了,虽然这样做可替代媒体查询,但当视窗宽度小于 320px(也就是定义的--min-viewport-width值) 还是会出现水平滚动条。不过不用担心,我们可以在 minmax() 函数中使用内在尺寸,即 min-content

body {
    --limit-container-width: 75ch; 
    --gutter: 1rem; 
    display: grid; 
    grid-template-columns: 1fr minmax(min-content, var(--limit-container-width)) 1fr; 
    gap: var(--gutter);
}

除此之外,还可以使用 CSS 函数中的比较函数 min()clamp()

/* CSS比较函数:min()  */
body { 
    --limit-container-width: 75ch; 
    --gutter: 1rem; 
    display: grid; 
    grid-template-columns: 1fr min(var(--limit-container-width), 100% - var(--gutter) * 2) 1fr; 
    gap: var(--gutter); 
}

/* CSS比较函数:clamp() */

body { 
    --limit-max-container-width: 75ch; 
    --limit-min-container-width: 320px; 
    --gutter: 1rem; 
    display: grid; 
    grid-template-columns: 
        1fr 
        clamp( 
            var(--limit-min-container-width), 
            100% - var(--gutter) * 2, 
            var(--limit-max-container-width) 
        ) 
        1fr; 
    gap: var(--gutter); 
}

也可以将min()minmax()混合在一起使用:

body {
    --limit-max-container-width: 75ch;
    --limit-min-container-width: 320px;
    --gutter: 1rem;

    display: grid;
    grid-template-columns:
        minmax(var(--gutter), 1fr)
        minmax(
        min(var(--limit-min-container-width), 100% - var(--gutter) * 2),
        var(--limit-max-container-width)
        )
        minmax(var(--gutter), 1fr);
    gap: var(--gutter);
}

这里提到的方案也常用来 构建 Full-Bleed 布局 ,如果你想更深入了解该布局,可以移步阅读《图解CSS:Grid布局案例之构建 Full-Bleed 布局》一文。

接着继续聊网格布局中网格项目引起的溢出问题。前面我们提到过,在Flexbox布局中,在Flexbox容器上未显式设置flex-wrap: wrap,并且Flex项目不具有伸缩性的话,Flexbox容器会产生滚动条。其实在 CSS Grid 布局中也有类似的现像。比如像下面这样的一个案例:

.card__content {
    display: grid;
    gap: 10px;
    grid-template-columns: repeat(3, 120px);
}

针对于这种现象,我们可以使用 CSS Grid 中的 auto-fillminmax() 来避免:

.card__content {
    display: grid;
    gap: 10px;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}

效果如下:

从《图解CSS:CSS 的值和单位》一文可以获知,CSS中可用单位有很多,其中很多单位也可以用于网格轨道尺寸的设置,比如pxrememvw百分比(% 以及CSS网格布局独有的fr单位。只不过,我们在设置网格轨道尺寸的时候,尽量不要使用 % 单位,尤其是显式设置了gap值(非%),因为这样做会引起溢出问题。比如:

.container {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(4, 25%);
}

这样设置会产生溢出问题的主要原因是gap的值被添加到网格容器的宽度上,而网格轨道尺寸取%时,它是相对于网格容器的宽度(或高度)来计算的。如果你希望避免这种现象出现,你需要做一些简单的数学计算:

.container {
    --columns: 4;
    --gap: 1rem;
    grid-template-columns: repeat(var(--columns), calc((100% - (var(--columns) - 1) * var(--gap)) / var(--columns)));
}

如果你讨厌做数学运算的话,还可以使用fr单位,它和百分比单位最大不同之处是将网格容器可用空间按fr数量均分。换句话说,使用fr时,会将gap从网格容器可用空间中扣除:

.container {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(4, 1fr);
}

上面代码表示的意思是,网格容器可用空间扣除 3rem(即 3 x 1rem)再均分成四个等份。为了能更好的与百分比对标起来,你还可以像下面这样使用fr

.container {
    grid-template-columns: repeat(4, 25fr);
}

用下图来展示 %fr 被用于设置网格轨道的差异:

具体示例效果如下:

定位元素引起溢出

前面有一个示例向大家展示了定位元素引起的溢出问题,甚至是在定位元素的父容器上显式设置了 overflowhidden同样会出现滚动条。在这里我们来一起看看是怎么回事。

首先要知道的是,定位元素满足下列条件是会引起溢出的:

  • 元素显示设置了positionabsolutefixedrelative,且使用 right为负值(或left为正值)将元素向右移动,则会产生水平滚动;或使用bottom为负值(或top为正值)将元素向容器底部外移动,则会产生垂直滚动。这两种方式都产生了溢出问题
  • 如果定位元素是非块元素,且positionrelative时,需要显式将该元素设置为块元素,比如display: block
  • 如果元素是固定定位(fixed)且不是相对于浏览器视窗定位(“固定定位一般是相对于视窗定位”),需要在其父容器上显式设置transform的值为translateZ(0)(其他类似的值也有效)

在实际生产中,你可能真的需要将元素定位到容器之外,但又不想产生滚动条。你可能首先会想到在其父容器上显式设置 overflowhidden。可事实上,在某些场景下是无法满足你的需求。

<!-- HTML -->
<div class="grandfather"> 
    <div class="parent"> 
        <div class="child" style="right: -100%"></div>
        <div class="child" style="left: -100%"></div>
        <div class="child" style="top: -100%"></div>
        <div class="child" style="bottom: -100%"></div> 
    </div> 
</div>

/* CSS */
.grandfather {
    position: relative;
}

.parent {
    overflow: hidden;
}

.child {
    position: absolute;
}

解决它的快速办法是,将overflow:hidden设置在.grandfather上。不过,这种现象有时候又是我们所需要的,比如下面这个示例:

示例中右侧的效果才是我们想要的效果:

一直以来,很多人认为使用了overflow:hidden属性就一定会把该容器所有的后代元素隐藏(如有溢出的话)。事实并非如此,比如文章最开始演示的案例。绝对定位的元素或伪元素并没有被父容器裁切掉。为什么会这样呢?简单地说,该示例触发了:

  • 拥有overflow:hidden元素并不具有position取非static的值
  • 内部元素通过position:absolute进行定位

一个绝对定位的后代块元素,部分位于容器之外。这样的元素是否剪裁并不总是取决于定义了overflow属性的祖先容器;尤其是不会被位于他们自身和他们的包含块之间的祖先容器的overflow属性剪裁。另外规范中也有说到:

当一个块元素容器的内容溢出元素的盒模型边界时是否对其进行剪裁。它影响被应用元素的所有内容的剪裁。但如果后代元素的包含块是整个视区(通常指浏览器内容可视区域,可以理解为body元素)或者是该容器(定义了overflow的元素)的父级元素时,则不受影响。

通常一个元素的包含块由离它最近的块级祖先元素的内容边界决定。但当元素被设置成绝对定位时,包含块由最近的position不是static的祖先元素决定。这样一来,知道问题是什么原因造成的就好办了。只需要在设置有overflow:hidden的元素上添加position属性,具值是非static即可。

长单词(或URL地址)引起溢出

在一些情景中,长单词或URL地址也会引起溢出问题。当容器的宽度小于元素的min-content(指的是内容最小宽度,一般是最长单词的长度),这个长单词就会溢出容器,引起溢出问题:

开发者可以根据自己需求来避免长单词引起的溢出问题,比如,使用CSS的overflow-wrap

.nav a {
    overflow-wrap: break-word;
}

另外一种方式是使用 text-overflow 将溢出的文本进行裁剪,并在末尾添加...(省略号指示器):

.nav a {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

这里需要注意的是,不管是使用 overflow-wrap: break-word 还是 text-overflow: ellipsis,在Flexbox的Flex项目或Grid的Grid项目中有一个必要条件,即**min-width必须显式设置为0**。为什么呢?Flexbox规范(W3C规范)这样描述来着:

“默认情况下,Flex项目不会缩小到其最小内容大小(最长单词或固定大小元素的长度)以下。要更改此设置,请设置min-widthmin-height属性”。

这也意味着,带有长单词的Flex(或Grid)项目不会缩小到其最小内容(即min-content的计算值)以下。到这里,我想你应该知道了,长单词引起溢出问题的原因所在。具体怎么避免,上面两个示例已经给出答案了。

在CSS网格布局中,除了在网格项目(一般是具有弹性伸缩的网格项目,即网格轨道为 1fr的网格项目)上显式设置 min-width: 0 之外,还可以使用别的处理方式。即:

.grid__container {
    display: grid;
    grid-template-columns: 280px minmax(0, 1fr)
}

这样一来,弹性网格项目的最小内容大小就不会是 auto

通过前面的内容,我们知道如何给单行或多行文本末尾添加省略号(...)指示器,但并不是说所有场景都可以使用使用 text-overflow: ellipsis 来实现。正如前面在介绍 text-overflow: ellipsis 时提到过,如果直接在匿名的Flex(或Grid)项目中使用,就无法达到预期的效果:

针对这种现象,如果希望它能达到你的预期效果,需要给匿名的Flex或Grid项目添加一个容器,比如给文本添加一个<span>标签:

<!-- 修改前 -->
<div class="container">我是匿名Flex或Grid项目</div> <!-- Flex or Grid-->

<!-- 修改后 -->
<div class="container">
    <span>我是Flex或Grid项目</span>
</div> <!-- Flex or Grid-->

/* CSS */
.container {
    display: flex; /* inline-flex | grid | inline-grid */
}

.container > span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

再来看一个关于 text-overflow: ellipsis 的实用案例。比如下面这样的一个场景,在我们平时的开发中也是很常见的:

设计师希望的是“徽标过多时,最后提供省略号指示器,并不是直接截断或断行”。对于开发者来说,可能会使用像下面这样的一个HTML结构来构建徽标列表:

<!-- HTML -->
<ul class="badges">
    <li>Fast food</li>
    <!-- ... -->
    <li>Fruits</li>
</ul>

你可能会认为直接在.badges上像处理文本溢出指示那样就可以达到效果:

.badges {
    display: flex;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* 或者 */

.badges {
    display: grid;
    grid-auto-flow: column;
    grid-template-columns: auto;
    grid-auto-columns: auto;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

这样的处理方式,最终在浏览器呈现出来的效果是“溢出容器的徽标被裁剪了”:

如果把 text-overflow 相关的样式设置到 <li> 标签上:

.badges li {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

虽然不会像上面一样,把溢出的徽标裁剪掉,但这样做的话,虽然能在容器空间中将徽标罗列出来,但会在每个徽标上添加省略号的指示器,也不符合预期效果:

如果li元素是一个Flex容器或Grid容器的话,达到上图效果还需要额外添加一个标签来包裹文本:

<ul class="badges">
    <li><span>Fast food</span></li>
    <!-- ... -->
    <li><span>Fruits</span></li>
</ul>

不过,在CSS中还是有方案可以达到设计师预期想要的效果:

达到上图的效果,在 CSS 中有两种方式可以实现,先来看第一种,即 使用line-clamp替代text-overflow

.badges {
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
}

.badges li {
    display: inline-flex; /* inline-block */
}

另外一种,还是继续使用text-overflow,但需要改变每个li视觉模型,即将 display 设置为 inline-block

.badges  {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.badges li {
    display: inline-block; /* inline-flex */
}

100vw引起溢出

视窗单位(比如vwvh等)使用频率是越来越多,很多开发者喜欢在 body 上显式设置 100vw,殊不知,这个 100vw 会在部分系统的浏览器中造成水平滚动。比如在 Windows 系统上,浏览器的垂直滚动条是默认显示,而这个 100vw 是包括滚动条的宽度,所以最终结果是您的页面比可用宽度稍宽。

注意,MacOS 只要 System Preferences > General > Show scroll bars 并未设置为 always 就不会产生水平滚动条。

这是因为带有垂直滚动条的网页需要将其 100vw 元素 + 滚动条宽度(大约12px ~ 20px) 压缩到 100vw 空间中,这只能通过添加水平滚动条来完成。因此水平滚动很小。

要修复 100vw 引起的溢出问题,最简单的方法是使用 100% 来替代 100vw,如果你实在想使用 100vw,你可以像下面这样使用:

body {
    width: 100vw;
    max-width: 100%;
}

或者使用 min()

body {
    width: min(100%, 100vw);
}

也可以将JavaScript脚本和CSS自定义属性结合起来使用:

/* CSS */
body {
    --scrollbar: 20px;
    width: calc(100vw - var(--scrollbar));
}

// JavaScript
const body = document.querySelector("body");
const scrollbar = window.innerWidth - body.clientWidth;
body.style.setProperty("--scrollbar", `${scrollbar}px`);

或者像下面这样:

/* CSS */
body {
    --vw: 1vw;
    width: calc(100 * var(--vw));
}

// JavaScript
const body = document.querySelector("body");
const vw = body.clientWidth / 100;
body.style.setProperty("--vw", `${vw}px`);

CSS尽力减少“数据损失”

我们已经花了很长的篇幅和大家聊溢出问题了。在 CSS 中,往往是设置 overflowautoscroll ,在内容溢出时产生滚动条避免数据损失。那么,什么是数据损失呢?

拿一个简单的示例来阐述吧,在你的Web中有一句话,由于你设置了一些样式,呈现给用户的只有半句话,另外半句有可能是被截取了,那么被截取的这半句话就对于用户而言就是数据损失。除此之外,在我们构建 Web 的时候,会使用现代的一些对齐方式属性,比如 justify-contentalign-items 等,虽然这些对齐特性让我们获得更好的对齐能力,但也有可能会造成数据的损失。比如前面提到的水平导航栏案例:

水平滚动容器不管是Flexbox还是Grid布局,如果显式设置了 justify-content 的值为 flex-endend,水平滚动将会失效。

这也是一种数据损失。或者简单地说,由于某些 CSS 的设置,造成了滚动失效。 比如下面这两个案例,就是典型的滚动失效让用户无法浏览被隐藏的数据。

上图中,虚线框中的内容会随着列表项增加产生垂直滚动条。大多数开发者会采用 Flexbox 来构建,并且在列表项的容器中显式设置 flex: 1

在滚动容器上显式设置了 overflow-y`` 为 scroll`` 也未生效(当时发现这个Bug的是在iOS系统中)。后来才发现,这只是触发了Flex项目的边缘情况。当然,实现这个需求,他对HTML结构是有一定的要求的:

对应上图结构图的 HTML 代码如下:

<div class="main-container"> <!-- flex-direction: column -->
    <div class="fixed-container">Fixed Container</div> <!-- height: 100px; -->
    <div class="content-wrapper"> <!--  min-height: 0; -->
        <div class="overflow-container">
            <div class="overflow-content">
            Overflow Content
            </div>
        </div>
    </div>
</div>

关键的 CSS 代码:

.main-container {
    display: flex;
    flex-direction: column;
}

.fixed-container {
    height: 100px;
}

.content-wrapper {
    display: flex;
    flex: 1;
    min-height: 0; /* 这个很重要*/
}

.overflow-container {
    flex: 1;
    overflow-y: auto;
}

具体示例效果如下:

关键部分是 设置了 flex: 1 的 Flex 项目需要显式设置 min-height 的值为 0 ,即滚动容器的父元素。这其实是 Flexbox 布局的一个边缘情况。

注意,在设置了 flex:1 的 Flex 项目中应该尽可能的重置它的 min-size 值(当主轴在水平方向时(flex-direction: row),设置min-width,当主轴在垂直方向时(flex-direction: column),设置min-height),一般情况下都将其重置为 0

另一个至Flexbox布局中滚动失效是 flex-end。简单地来看一下这个案例:

<!-- HTML -->
<Container>
    <Card />
    <Card />
    <Card />
    <Card />
</Container>

/* CSS */
.container {
    display: flex;
}

.container--row {
    overflow-x: scroll;
}

.container--column {
    flex-direction: column;
    overflow-y: scroll;
}

这是正常情况之下,没有使用任何对齐方式相关的属性,比如 justify-content。整个效果如下:

如果我们在滚动容器 .container 显式设置 justify-content 的值为 flex-end

.container {
    justify-content: flex-end;
}

这个时候滚动失效:

在 Web 中,我们的书写习惯和阅读模式是从左到右(LTR),从上到下。也就是说,一般情况之下(先不考虑其他的书写模式),水平方向内容向右溢出,垂直方向的内容向底部溢出,即 滚动条在设计的时候,就约定了,只有容器下方(或右侧)内容有多余,才需要滚动。

在滚动容器(它刚好是Flexbox容器)显式设置了 justify-content: flex-end(主轴方向从 flex-start 换成了 flex-end )。这就导致,如果有内容是在上方或左侧超过容器的尺寸限制,滚动条是不会有任何变化的。也因此,滚动容器里面内容溢出容器的方向不是在容器的下方或者右侧,而是在容器的顶部和左侧,自然就无法触发滚动条的出现。简单地说,flex-end 会让内容反向溢出,也就没有滚动条,自然也就无法滚动:

在这种情况之下,想让滚动容器能正常滚动起来,其实很简单,借助 margin: auto 即可。在Flex项目是显式设置 margin 的值为 auto 有着独特的效果,如下图所示:

解决方法很简单,对齐方式开始默认的对齐,即 justify-content 不设置为 flex-end,取默认值,然后使用 margin: auto 实现类似justify-content: flex-end对齐效果。在我们的示例中,只需要给第一个Flex项目设置margin-left(水平方向)或 margin-top(垂直方向)的值为auto

.container--row .card:first-child {
    margin-left: auto;
}

.container--column .card:first-child {
    margin-top: auto;
}

点击示例中的按钮动态增加卡片:

小结

到了这里,表示有关于 CSS 溢出的话题就要结束了。我们花了很长的篇幅来阐述CSS溢出相关的内容,主要是因为,了解溢出相关的原理,你就会更容易理解溢出内容在布局方法以及溢出的工作方式,从而让你构建出更健壮的Web页面,尤其是布局方面,不再会因为溢出打破你原有布局。