Skip to content

Web 开发人员在开发过程中时常会遇到容器出现水平滚动条的问题,尤其是在移动终端上。由于导致容器出现滚动条的原因有很多,因此没有直接的解决方案。有些问题可以很快解决,有些则需要一些调试技巧。在这节课中,我们将探讨溢出问题的原因以及如何解决这些问题。我们还将探索开发者工具(DevTools)中的现代功能如何使修复和调试过程更容易。

什么是溢出?

CSS 盒模型告诉我们,在 CSS 中万物皆是盒子:

img

因此,CSS 中允许你使用 width(或 inline-size)和 height(或block-size)等属性给盒子的尺寸进行约束。既然万物皆盒,且盒子有大小,那么盒子就有可能没有足够的空间来容纳其内容。也就是说,溢出是在你往盒子里塞太多东西的时候发生的,所以盒子里的东西也不会老老实实呆着。

img

正如上图所示,左侧内容超出了容器,产生了内容溢出;右侧因内容少,未产生内容溢出。

你一旦想改变 Web 布局或 UI 风格,你就得开始把一些控制权掌握在自己手中了。在某些情况下,你也需要把处理溢出的工作交给自己,即要自己处理溢出问题并创造出不那么脆弱的设计

CSS 的溢出模块(CSS Overflow Module: Level 3Level 4)提供了一些 CSS 特性来控制溢出。开发者理解这些概念和掌握这些特性是很有用的,因为了解溢出的行为对于处理 CSS 中大小受限的任何元素都很重要。

img

处理溢出的常见方式

到目前为止,CSS 处理溢出的方式主要有:剪切溢出指示溢出分段溢出

剪切溢出

首先来看剪切溢出。剪切溢出指的是在容器上使用 overflow 来控制溢出内容的方式,其中 overflow 的值为 autoscroll, 让溢出容器产生滚动条,滚动条有可能是水平的,也有可能是垂直的:

img

指示溢出和分段溢出

在 CSS 的设计中,溢出的内容可以是任何东西,尤其是文本内容,很多时候一些长单词、url 地址、非法内容等很容易会产生溢出,严重的还会打破布局。

虽然 overflow 可以很好地帮助我们控制溢出内容在浏览器中的表现行为,但在某些情况之下,比如直接剪切溢出内容(overflow 取值为 hiddenclip),用户体验并不很好。如果需要给用户一个更好的体验,我们应该给用户一个提示,告诉用户还有内容未展示出来。这个提示,我们称之为“指示器”。

回过头来看,文本溢出主要会在容器的内联轴(Inline Axis)方向和块轴(Block Axis)方向:

  • 内联轴方向 :文本容器设置了 white-space: nowrap 阻止文本断行或单个单词太长造成容器无法容纳,文本溢出容器;
  • 块轴方向 :文本内容过多,造成文本区域的高度超出容器的最大高度,容器无法容纳。

针对这些现象,如果希望给用户更好的体验,往往会在被裁剪的内容最末尾添加一些指定符号,比如三个点,即用指示器告诉用户内容未全部展示,如果需要查阅全部内容需要做一些额外的交互操作(如果有的话)。

如今,CSS 就提供相应的特性,分别可以在内联轴和块轴方向为被裁剪的内容提供相应的指示器。根据不同方向,我们也称之为 “指示溢出” 和 “分段溢出”:

img

指示溢出和分段溢出都有相应的 CSS 属性来控制。

text-overflow 可用来控制单行文本溢出。你可以在任何包含文本节点的元素上使用 text-overflow 属性,它指定了当文本不适合该元素的可用空间时如何出现。简单地说,确定如何向用户发出未显示的溢出内容信号,它可以直接将溢出的内容裁剪掉,也可以渲染一个省略字符来表示被裁剪掉的内容。

img

不过,text-overflow 本身并不会强制“溢出”事件的发生(并不能直接裁剪文本),因此要让 text-overflow 生效,必须满足相应的条件:

  • 文本节点的容器是一个块容器,即容器元素是一个块级元素或显式设置 displayblockinline-blockflow-rootlist-item 等;
  • 溢出的内容必须要与块级元素内联轴方向一致;
  • 文本不断行,比如使用 white-space: nowrap 强制不断行或者一个单词因为太长而不能合理地被安置;
  • 文本节点的容器有具体的宽度,比如显式设置 widthmax-width(相对应的逻辑属性也可) ;
  • 文本节点容器上设置了 overflow 是一个非 visible 的值,一般将 overflow 设置为 hidden

也就是说,要让 text-overflow 属性生效,需要结合其他几个属性一起使用,比如:

CSS
.ellipsis { 
    max-width: 30ch; 
    white-space: nowrap; 
    text-overflow: ellipsis; 
    overflow: hidden; 
 }

img

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

需要注意的是,文本节点容器是一个 Flexbox 容器(即 display 显式设置为 flexinline-flex)或 Grid 容器(即 display 显式设置为 gridinline-grid)的话,文本节点会产生一个匿名盒,而且它不是一个块级匿盒子,分别是一个 Flex 级别(flex-level boxes)和 Grid 级别(grid-level boxes)的匿名盒子。在这种情况之下,text-overflow 是无法工作的。

HTML
<div class="flex">Loooooooooooooooooooooooong Text</div> 
<div class="grid">Looooooooooooooooooooooong Text</div>
CSS
.flex { 
    display: flex; /* 或 inline-flex */ 
    white-space: nowrap; 
    max-width: 10ch; 
    overflow: hidden; 
    text-overflow: ellipsis; 
} 

.grid { 
    display: grid; /* 或 inline-grid */ 
    white-space: nowrap; 
    max-width: 10ch; 
    overflow: hidden; 
    text-overflow: ellipsis; 
}

img

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

你只需要在文本外套一个容器即可避免这个现象:

HTML
<div class="flex">
    <div>Loooooooooooooooooooooooong Text</div> 
</div>
<div class="grid">
    <div>Looooooooooooooooooooooong Text</div> 
</div>

我们知道了如何在单行文本溢出后面添加指示符号(一般是省略号,即 ...),现在我们来看如何给多行文本的末尾处添加指示符号。

CSS 的 line-clamp 允许你把块容器中的内容限制为指定的行数内,剩余的内容被分割开来,既不被渲染也不被测量。

img

可选的是,它还允许将内容插入最后一行盒子,以表示被截断(或中断)的内容的连续性。

CSS
.line-clamp { 
    line-clamp: 4 "... (continued on next page)" 
}

img

标准的 line-clamp 还是 max-linesblock-ellipsiscontinue 三个属性的简写属性。

不过,需要注意的是,在目前为止,要实现多行截断的效果只能使用 -webkit-line-clamp,而且只有在 display 属性设置为 -webkit-box-webkit-inline-box-webkit-box-orient 属性值是vertical 时才生效:

CSS
.line-clamp { 
    display: -webkit-box; /* 或 -webkit-inline-box */ 
    -webkit-box-orient: vertical; 
    -webkit-line-clamp: 3; 
    overflow: hidden; 
}

在大部分情况下,也需要设置 overflow 属性为非 visible , 否则里面的内容不会被裁减,并且在内容显示为指定行数后还会显示省略号。

img

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

正如上面示例所示,当 -webkit-line-clamp 的值为 1 时,在浏览器中呈现给用户的效果与 text-overflow: ellipsis 是一样的(等同于指示溢出)。只不过在使用 -webkit-line-clamp 实现类似 text-overflow: ellipsis 效果时,不要设置 white-space 的值为 nowrap,否则单行末尾不会有省略号:

img

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

虽然 -webkit-line-clamp 实现的视觉效果和标准的 line-clamp 相同,但它们的行为却不尽相同。

-webkit-line-clamp 的行为很古怪,也不太稳健,@Nils Rasmusson 的 《CSS Line-Clamp — The Good, the Bad and the Straight-up Broken》文章中就有过记载。标准的 line-clamp 的设计是从早期实验的错误中吸取了教训,旨在与现有的内容充分兼容,最终可以改变其实现方式,以遵循指定的行为。

line-clamp 属性实现分段溢出很容易,但它也存在一定的缺陷。比如说,你想给元素添加 padding,分段溢出的效果就很容易失败。如下面这个示例:

CSS
.line--clamp  { 
    display: -webkit-box; 
    -webkit-box-orient: vertical; 
    -webkit-line-clamp: var(--line-clamp, 2); 
    overflow: var(--overflow, hidden); 
    padding: var(--padding, 1rem); 
}

img

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

正如你所看到的,当溢出容器设置了 padding: 1rem 时,浏览器中呈现的效果并不是你所期望的:

img

同样的,在使用 line-clamp 时,不要在元素上显式设置 heightmin-height 以及它们对应的逻辑属性,不然的话,当 height 的值小于或大于限制行数的总高(一般是行高乘以限制的行数)时,就会产生类似于设置了 padding 值的效果:

CSS
.line--clamp p {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: var(--line-clamp, 2);
    overflow: var(--overflow, hidden);
    height: var(--height, auto);
}

img

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

Web 设计时不需要的溢出

事实上,现代 Web 布局的方式可以很好地处理溢出。我们不一定能预料到 Web 上会有多少内容,人们很好地设计它们,使得它们能与这种现状协调。

但是在以往,开发者会更多地使用固定高度,尽力让毫无关联的盒子的底部对齐。这是很脆弱的,在旧的应用里面,你偶尔会遇到一些盒子,它们的内容遮到了页面上的其他内容。如果你看到了,那么你现在应该知道,这就是溢出,理论上你应该能重新排布这些布局,使得它不必依赖于盒子尺寸的调整。

在开发网站的时候,你应该一直把溢出的问题挂在心头,你应该用或多或少的内容测试设计,增加文本的字号,确保你的 CSS 可以正常地协调。改变溢出属性的值,来隐藏内容或者增加滚动条,会是你仅仅在少数特别情况下需要的,例如在你确实需要一个可滚动盒子的时候。

你现在明白,CSS 会尽力避免溢出的内容不可见,因为这会造成数据损失。你已经发现,你可以控制住潜在的溢出,同样,你也应该测试你的作品,确保你不会一下子就弄出令人困扰的溢出。

溢出问题的排查技巧

在 Web 开发过程中,overflowtext-overflowline-clamp 等属性根据需要设置不同的溢出模式,但在实际使用过程中,总是会碰到一些离奇的现象,让人琢磨不透。比如下面这样的一个示例,使用空元素来绘制图形(模拟一张背景氛围图)。按照常理,我们在容器上显式设置了:

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

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

img

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

正如你所看到的,虽然显式设置了 overflow-x: hidden,水平方向还是会出现滚动条,溢出的内容并没有被裁切。对于这样的现象,往往喜欢说成使用 overflow 碰到的坑或者说无法正常工作。

那么在实际使用的过程中,常常会碰到一些什么样的坑呢?碰到这些所谓的坑,有时候能很快定位到问题所在,并且解决掉,不过有些坑,需要一些调试的技巧。

什么是溢出问题?

拿上面这个案例为例,你已经给容器指定了 overflow-x: hidden ,容器不应该出现水平滚动条。事实上,容器却无意中出现了一个水平滚动条,这种现象就是一个溢出问题。这可能是由不同的因素造成的,可能是因为内容出乎意料的宽,或者固定宽度的元素比视口宽。我们将在接下来的内容中探讨所有的原因。

如何发现溢出?

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

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

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

其次,使用JavaScript 来检测溢出。你可以在浏览器调试工具中添加 JavaScript 脚本,把 Web 页面中比 body 元素更宽的元素都打印出来:

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

img

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

当然,你也可以使用 CSS 检测法。就我个人而言,以往在排查 CSS 的问题时,总是喜欢在有问题的元素上添加一个 outline

CSS
element { 
    outline: 1px solid yellow; 
}

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

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

img

也可以使用 @Addy Osmani 的 CSS 布局调试器,让布局可视化。用随机的(有效的) CSS 十六进制颜色勾勒出页面上的每个 DOM 元素。你只需要把下面这段脚本放到你的页面中或者在浏览器调试器中输入:

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

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

img

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

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

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

常见的溢出问题

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

固定尺寸引起溢出问题

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

CSS
.element { 
    width: 368px; 
}

img

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

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

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

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

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

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

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

当然,你也可以使用 CSS 中的比较函数(比如 min()max()clamp() )给元素设置尺寸,它们能从函数中的一系列值中选择最为合适的值。就拿 min() 函数为例,它可以像下面这样替换上面的代码:

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

img

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

width 类似,假设你不经思考就给元素的 height 指定一个固定值,那么该元素就很容易产生溢出问题:

CSS
.element {
    height: 120px;
}

img

所以说,即使你在开发过程中,要给一个元素设置高度,也不应该设置一个固定值,除非你能完全预见它不会因此引起溢出问题。比较妥当的做法是,给元素设置一个 min-height 或者使用 min()max()clamp() 函数给 height 设置值:

CSS
.element {
    min-height: 120px;
}

/* 或者 */
.element {
    height: min(100%, 120px);
}

img

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

未使用 flex-wrap 的 Flexbox 布局

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

img

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

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

img

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

网格布局引起的溢出

CSS 网格布局是 Web 布局中的唯一一种二维布局,它很强大,同时也很复杂。在使用 CSS 网格布局时很容易产生溢出问题。

例如,你可以像下面这样,使用 CSS 网格来构建一个水平居中的布局效果:

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

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

它有点类似于:

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

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

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

img

根据《固定网格轨道尺寸给 Web 布局带来的局限性》课程中所介绍的那样,在 CSS 中有多种方式可以避免这种现象,最常见的一种方式是使用 CSS 的媒体查询 @media,只有在足够多的空间下才设置多列网格。我们可以像下面这样来调整代码:

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

@media only screen and (width > 768px) { 
    body { 
        grid-template-columns: 1fr 75ch 1fr; 
    } 
    
    .container { 
        grid-area: 1 / 2 / -1 / 3; 
    } 
}

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

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 的自定义属性来对视窗“最大值”、“最小值”和“网格间距”进行定义:

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; 
}

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

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

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

CSS
/* 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; 
} 

/* 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; 
}

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

CSS
 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); 
 }

这里提到的方案也常用来构建 Full-Bleed 布局,它也是 Web 布局中的经典布局。

img

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

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

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

img

针对于这种现象,我们可以使用 CSS Grid 中的 RAM 布局技术(repeat()auto-fillauto-fitminmax() )来避免:

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

你甚至还可以在 minmax() 函数中引入 min()max()clamp() 函数:

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

img

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

在 CSS 网格布局中,允许你使用百分比(%)值设置网格轨道尺寸。只不过,在设置网格轨道尺寸的时候,尽量不要使用 % 单位,尤其是显式设置了 gap 值(非 %),因为这样做会引起溢出问题。比如:

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

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

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

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

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

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

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

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

img

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

定位元素引起溢出

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

img

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

在这里我们来一起看看是怎么回事。

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

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

img

在实际生产中,你可能真的需要将元素定位到容器之外,但又不想产生滚动条。你可能首先会想到在其父容器上显式设置 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; 
 }

img

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

img

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

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

简单地说,该示例触发了:

  • 拥有 overflow:hidden 元素,并且该元素的 position 没有取非 static 的值 ;
  • 内部元素通过 position:absolute 进行定位 。

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

另外规范中也有说到:

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

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

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

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

img

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

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

img

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

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

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

img

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

这里需要注意的是,不管是使用 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 之外,还可以使用别的处理方式。即:

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

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

有关于这方面更详细的介绍,可以移步阅读《Flexbox 和 Grid 中的最小尺寸》。

100vw 引起溢出

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

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

这是因为带有垂直滚动条的网页需要将其 100vw 元素 + 滚动条宽度 (大约 12px ~ 20px) 压缩到 100vw 空间中,这只能通过添加水平滚动条来完成。因此水平滚动很小。 要修复 100vw 引起的溢出问题,最简单的方法是使用 100% 来替代 100vw,如果你实在想使用 100vw,你可以像下面这样使用:

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

或者使用 min()

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

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

CSS
body { 
    --scrollbar: 20px; 
    width: calc(100vw - var(--scrollbar)); 
} 
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)); 
}
const body = document.querySelector("body"); 
const vw = body.clientWidth / 100; 
body.style.setProperty("--vw", `${vw}px`);

小结

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