你所不知道的CSS Overflow Module

发布于 大漠

最近在项目中使用overflow属性的时候踩到了几个以前从未踩过的坑。在填坑的过程中发现,原来使用overflow的时候还是有不少的坑,而且这些坑都是因为自己对该属性不甚了解所造成的,或者说是和其他CSS属性在一起使用所触发的。那么在使用overflow应该怎么使用才能避开这些不必要的烦恼呢?或者说在使用overflow不应该和哪些属性结合在一起使用呢?为了解开这个迷我重读了有关于overflow的规范。今天将相关的理解和新的认知小结一下与大家共享,希望对于大家在实际使用的时候能尽可能的避开这些坑。

触发overflow坑的场景

触发这个坑也是无意之中触发的,毕竟以前从未触发到。那么是怎么样的一个场景触发的呢?这个场景很简单:

借助CSS的能力绘制一个类似下图的背景效果

虽然在容器.root中设置了容器的尺寸,并且overflow-x设置了hidden:

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

注意,代码中显式的设置了overflow-x:hidden。按照我们对overflow属性的认知,横向溢出.root容器的内容都应该会被截取(看不见)。但事实上并非如此,你看到的效果却如下,水平方向还是会出现滚动条,溢出的内容并没有被裁切

尝试着把.root中的overflow-x:hidden移动到body元素中:

body {
    width: 100vw;
    height: 100vh;
    background: #D91B1A;
    overflow-x: hidden;
}

如此一来,你会发现在桌面端符合我们的预期,溢出内容会被裁切,但千万别高兴的太早,移动端(不管是Android还是iOS系统下),问题依旧。我继续往html元素设置overflow-x:hidden仍然在移动端上无法正常工作。 那么这是什么呢?先不解答其中原委,给大家留下一个噱头,感兴趣的同学请继续往下阅读。

有关于CSS Overflow Module

到目前为止,有关于CSS Overflow Module主要有两个版本,即 Level 3Level 4。在Level 3版本中涵盖了我们常见的overflow的使用,也有一些我们不常见的,而且text-overflow这样的属性也纳入到了Level 3版本中(被纳入到Automatic Ellipses)。

overflow的类型

在CSS中的overflow主要作用是用来描述一个扩展到该框的边缘,即内容边缘填充边缘边框边缘外距边缘。简单地说就是如何用来控制溢出,从而不让溢出的内容来影响其他元素的布局。从规范中看,有关于overflow主要分为两种类型:墨水溢出(Ink overflow)和 可滚动的溢出(Scrollable overflow)。

墨水溢出

墨水溢出的盒子是盒子的一部分,它的内容创造了一个视觉浆果以外的盒子的边框(border-box)。墨水溢出是绘画(Painting)的溢出,主要用来定义不影响布局或其他方式扩展滚动溢出(Scrollable overflow),比如box-shadowborder-imagetext-decorationoutline等。

在CSS中,有些属性(比如text-shadowbox-shadow)在盒子定义中是一种模糊的,并没有定义它们所覆盖的视觉范围(理论上是无限的),所以墨水溢出也没有一个明确的范围定义。

墨水溢出区域(Ink overflow region)是一个非矩形区域,墨水矩形是坐标轴与盒子坐标轴对齐,并包含墨水溢出区域的最小矩形。注意,墨水溢出矩形在盒子坐标系中是一个矩形,但是由于transform在其他坐标系中可能是非矩形的

请注意,这里可能就给我们在使用overflow时埋下了一个坑:overflowtransform一起使用时,可能有深坑存在!

可滚动溢出

可滚动溢出(Scrollable Overflow)可以说是我们比较熟悉的溢出类型。滚动溢出和墨水溢出类似,他也有框的概念存在。滚动框的可滚动溢出是扩展到该框的padding边缘之外的一组内容,需要为其提供滚动机制。

可滚动溢出区域是可滚动溢出所中用的非矩形区域,而可滚动溢出矩形是最小矩形,其轴和框的轴对齐,并且包含可滚动溢出区域。常见的可滚动区域有:

  • 盒子本身的内容和填充区域(有关于内容和填充区域目前还存在一定的争议,有关于这方面的争议在这里不做相关阐述
  • 所有行框(line box)都直接包含在该框中
  • 它是包含块的所有框的边框盒子(border-box),并且其边框盒子并不完全位于块开始(block-start)或内行开始(inline-start)填充边缘(padding)之外,通过将每个框投射到元素的平面上来实现转换,从而创建3D渲染上下文(3D rendering context)
  • 上面所有框的可滚动溢出区域有一个前提,即可它们本身受具有overflow:visible(它们本身不会捕获溢出),并且可滚动溢出尚未被剪切(例如clipcontains

可滚动溢出矩形在框本身的坐标系统中始终是矩形,但由于transform,在其他坐标系统中可能是非矩形。这意味着滚动条有时会在实际上不需要的时候出现。

Overflow模块属性的划分

上一部分从概念上介绍了溢出的两大类型,其中可滚动溢出是我们较为熟悉的一种溢出。如果对于概念或理论上的理解较为无聊的话,我们可以来看看溢出模块中的属性划分,即我们在实际使用中会用的属性:

  • 用于控制盒子可滚动或剪切的属性:overflowoverflow-xoverflow-y
  • 流相关的属性:overflow-blockoverflow-inline
  • 文本溢出(溢出省略号)属性text-overflow和指示块轴溢出属性block-overflow
  • 分割溢出(Fragmenting Overflow)属性:line-clampmax-linescontinue

事实上在Level 3中对上述属性的使用做出相关的描述和解释,而在Level 4中新增了scrollbar-gutter并且对Fragmentation溢出属性continue进行更详细和深入的介绍。

在接下来的内容中可能会涉及到一些CSS概念,比如文档流滚动条盒模型书写模式等,如果你暂对这方面概念了解不是很深入的话也没有太大关系,你可以通过下面的内容了解到Overflow模块中各个属性的基本使用以及如何使用,何时使用。待到一定的时候,回过头来阅读下面的内容可能会更有味道或体会。

滚动和剪切溢出属性

overflowoverflow-xoverflow-y是我们最熟悉,也最常用的溢出属性,这些属性可以指定容器中溢出的内容如何展示,是直接溢出容器,还是剪切溢出容器的内容,还是容器出现滚动条让溢出的内容在容器中显示。这三个属性都接受下面几个属性值:

  • visible:默认值。溢出容器的内容不会被剪切,将溢出容器框之外
  • hidden:溢出容器的内容将被剪切,并且容器不会出现滚动条
  • scroll:内容是否溢出,容器都会出现滚动条,溢出的内容会被容器剪切,但可以通过滚动条来查看被溢出的内容
  • auto:取决于用户代理。如果内容适合填充框内部,则看起来与可见内容相同,但仍然会建立新的块格式化上下文。如果内容溢出,其行为和scroll类似
  • inherit:继承父元素的溢出属性

其中overflowoverflow-xoverflow-y的简写属性:

  • overflow-x属性指定处理水平方向上的溢出
  • overflow-y属性指定处理垂直方向的溢出

另外,overflow-xoverflow-y仅接受一个属性值,但overflow属性可以接受两个属性值,第一个值表示x轴的溢出属性值,第二个值表示y轴的溢出属性值。如果省略第二个值将表示xy轴都将采用相同的溢出属性值。

假设我们容器#box大小为300px x 300px,而其子元素(或后代元素)的尺寸大小为50vw x 50vw,如果视窗足够大,看到的效果如下:

因为容器没有显式设置overflow的值,那么客户端样式会以overflow的默认值visible属性来渲染。

注意,视窗单位vw是一个相对单位,在足够小的情况之下,.ele元素计算出来的尺寸会比容器#box的尺寸小,此时不管overflow设置为何属性值,表现都将趋于相似,不过还是会创建不同的格式化上下文。

正常情况之下,overflowoverflow-xoverflow-y取不同的属性值的表现如下:

更详细的效果可以通过下面之个小示例来体验:

流相关属性

流相关属性主要有overflow-inlineoverflow-block。它们将会按照CSS逻辑属性中定义的流相关盒模型模式来进行处理。

有关于逻辑属性和逻辑性给盒模型带来的相关变化可以花点时间阅读《理解CSS的逻辑属性和值》和《CSS的逻辑属性对盒模型带来的变化》相关教程。

overflow-inlineoverflow-block的取值和overflow一样。不同的是,overflow-inlineoverflow-block都有可能是overflow-xoverflow-y,具体映射要取决于CSS的书写模式属性,即writing-mode

自动省略属性

自动省略属性包含本文省略text-overflow和指示块轴省略block-overflow。其中text-overflow属性大家比较熟悉,很多时候文本溢出截取的时候末属会以三个点...的省略号为表示:

text-overflow属性接受clipellipsis<string>三个值:

  • clip:溢出的文本会直接被裁切(有点类似overflow:hidden效果)
  • ellipsis:溢出的文本会被裁切并且在末尾(或文本开头,根据direction或书写模式来决定)会添加三个点...这样的省略号
  • <string>:溢出的文本被裁切,并且可以在末尾或文本开头添加自定义字符串

使用text-overflow属性实现单行文本溢出添加省略号需满足下面相关条件:

  • 该元素必须为 display:block 或者 display:inline-block 元素(或者功能对等,比如Flex项目)
  • 元素必须得设置了widthmax-widthflex-basis
  • 该元素必须设置 overflow: hidden | scroll | auto
  • 文本溢出方向依赖 direction: rtl | ltr 属性
  • 整行溢出依赖 white-space: nowrap 属性

注意,取值为<string>目前只在Firefox下有效果,上面的示例使用Firefox查看,效果更佳

text-overflowoverflow属性类似,在某些情况之下也会失效,需要通过一些小技巧来规避。稍后我们会来聊一些这样的小技巧。

Fragmenting Overflow

上面看到的是单行文本溢出被截取,然后添加省略号。除此之外,我们还有多行文本后面添加省略号。实现这个效果同样可以使用text-overflow,但目前仅在Webkit内核下可用。WebKit内核的浏览器通过添加一个-webkit-line-clamp的私有属性来实现,但它需要组合其他的属性配合:

  • display: -webkit-box 将对象作为弹性伸缩盒子模型显示
  • -webkit-box-orient 设置或检索伸缩盒对象的子元素的排列方式
  • text-overflow: ellipsis 用省略号隐藏超出范围的文本

具体代码参考如下:

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

到目前为止,line-clamp得到主流浏览器的支持:

line-clamp是Fragmenting Overflow中的一个属性。

指示块溢出省略属性block-overflow事实上是line-clamp的一个子属性之一。该属性允许将内容插入(强制或非强制)区域中断之衫的最后一行框,以指示截断或中断内容的连续性。它只是影响块容器本身直接包含的行框,但是由于它是继承的,所以除非被覆盖,否则它将对后代的行框产生影响。如果该框不包含紧接在区域分隔符之前的行框,则此属必无效。该属性的值同样包含clipellipsis<string>

简单地说,block-overflow通常配合max-lines属性一起设置在多行后插入省略号或自定义字符串来指示后面的内容。

.module {
    block-overflow: [clip | ellipsis | <string>];
    max-lines: [<integer>]; /* required by block-overflow */
}

这个max-lines属性可以让你强制在多少行之后截图。你会发现,line-clamp和下面的代码是具有等同的效果:

line-clamp: 4;

// 等同于

block-overflow: ellipsis;
max-lines: 4;

碎片溢出(Fragmentation Overflow)还有一个continue属性。该属性有点类似一起CSS片段模块(CSS Fragmentation Module Level 3)中的box-decoration-break属性。可以将不适合的内容进行分割,并为剩余的内容应该继续的位置提供替代方案。有点类似下图这样的:

continue包含了下面五个属性:

  • auto:如果元素是CSS区域而不是区域链中的最后一个区域,该值只能作为计算值出现。不合适的内容会被推到链的下一个区域中展示
  • overflow:根据overflow属性来处理不适合溢出的内容
  • pagginate:不适合分页的内容。这将在元素中创建分页视图,类似于overflow:scroll创建可滚动视图的方式
  • fragments:不匹配的内容会导致元素复制自己并继续布局
  • discard:不适合的内容会在碎片片段中断时被丢弃

来看取值为fragments的几个小示例。

显式的设置了continue值为fragments时,如果产生了多个碎片片段,其提供了类似:nth-child这样的伪选择器,即::nth-fraggment()伪元素,为不同的碎片添加不一样的样式:

上面这些是有关于CSS Overflow Module Level 3Level 4 规范中所涉及到的一些东西。其中有很多属性还只是实验性的属性,或许以后会有较大的变化,这里提到的也仅供参考或扩展您的思路。

使用overflow碰到的坑

接下为,我们来聊点实际的,可用的东西。正如文章最开始提到的,使用了overflow:hidden并没有让溢出的内容被容器裁切。对于这样的现象我们往往喜欢说他是使用overflow碰到的坑或者说无法正常工作。那么在实际使用的过程中,常常会碰到一些什么样的坑呢?感兴趣的话,请继续往下阅读。

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

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

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

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

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

事实上这种情形并非一无事处,很多时候我们往往又需要让绝对定位的元素不会被设置了overflow:hidden的元素隐藏。比如Tooltips:

对于上面这个示例的场景,我们需要一个这样的结构:

<div class="grand-parent">
    <div class="parent">
        <div class="child"></div>
    </div>
</div>

样式简单的如下:

.grand-parent {
    position:relative;
}
.parent {
    overflow:hidden;
}
.child {
    position:absolute; 
    top:-10px; 
    left:-5px;
}

另外overflowposition:fixed元素在一起使用的时候,fixed元素将会打破overflow容器的束缚。比如我们有这样的一个结构:

<div>
    <img class="clip" src="https://images.unsplash.com/photo-1545569341-9eb8b30979d9?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=2100&amp;q=80">
</div>

div {
    width: 50vw;
    height: 50vh;
    overflow: hidden;

    img {
        position: fixed;
        top: 2vw;
        left: 2vw;
    }
}

众所周知,position:fixed在无特殊属性的限制的时候,其定位是相对于视窗边缘来定位的。即使他的父容器设置了overflow:hidden同样也会失效。比如上面这个示例,图片打破了设置overflow:hidden的容器div

如果要破这样的局,让fixed的元素也会被剪切,我们可以借助transform来搞定,即**transform除了none之外的任何值都会创建层叠上下文和包含块**。那该元素就会是固定元素的包含块。

div {
    transform: translateZ(0)
}

在未来,可以使用CSS Containment达到等同的效果。使用contain: paint元素作为一个包含绝对定位和固定定位后代的块

如果你对CSS Containment相关的介绍感兴趣的话,可以阅读《CSS Containment in Chrome 52》 和 《五个最新的CSS特性以及如何使用它们》。

事实上,CSS的transform是非常优秀的,当然也是相当复杂的。它对普通元素的渲染是有较大影响的,如果你对这方面感兴趣,可以移步阅读:

上面我们聊的主要是overflow:hidden无法正常工作的两个案例。事实上在实际使用的时候估计还会有潜在的其他现象。只不过我到目前还未碰到过,无法在这里和大家一起探计,如果你碰到相关的用例,欢迎在下面的评论中与我们一起分享

接下来,再来聊两个text-overflow的案例,第一个是在inline-flex中实现文本溢出。

在一个inline-flex元素中使用text-overflow: ellipsis并无法达到文本溢出裁切并在末尾添加...的效果:

div {
    display: inline-flex;
    width: 200px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

如果要在inline-flex元素上实现这个效果,需要在inline-flex元素内嵌一个元素:

<div class="inline-flex-ele">
    <div class="text-overflow-ele"></div>
</div>

.inline-flex-ele {
    width: 100px;
    white-space: nowrap;
}   
.text-overflow-ele {
    overflow: hidden;
    text-overflow: ellipsis;
}

对比效果如下:

另一个也是想在Flex 项目是实现text-overflow: ellipsis的效果,但又不想给Flex 项目显式设置widthmax-widthflex-basis之类的值。在Flex项目中,我们只需要借助min-width: 0text-overflow: ellipsiswhite-space: nowrap即可实现:

这让我想起了2009年迅雷UED出的一个面试题:

题目要求:日期紧跟新闻标题之后,而新闻标题不等长且要求超长截断,截断的文本最好能显示...省略号!

当年这个技术范畴中加上各大浏览器兼容性的要求,还是面挂了不少同学。当然在2009年那个时候实现这个效果也有很多种方法,那拿到今天来说,实现这个效果真是一如反掌,太简单不过了。我简单的写了一下:

这个示例用到了 CSS Counters 相关的特性来生成数字系列号,如果你对这方面从未接触过,建议你花点时间了解一下 CSS Counters,是一个非常有意思的特性,还能帮你写一些简单的小游戏

小结

本文从overflow:hidden失效的案例开始,重新探索了CSS Overflow Module Level3 和 Level4 规范中的一些属性,再次对Overflow 模块有了新的认知。这两个版本除了我们熟悉的overflowoverflow-xoverflow-ytext-overflowline-clamp等属性之外,还有很多实验性属性,对于这些实验性属性未来还会有一定的变化。当然这些属性一旦得到浏览器的支持,将会让我们实际工作变得更为灵活和轻松。

在了解完模块中的属性之外,我们还简单的了解了CSS中使用Overflow模块碰到的一些坑,比如overflow:hidden失效,比如说Flex项目中怎么使用text-overflow能生效等。当然除了这些坑之外,肯定还会有其他的,如果你在实际开发中碰到相关的坑,欢迎在下面的评论中把你填过的坑与我们一起共享。