图解CSS:CSS溢出(Part1)

发布于 大漠

溢出是CSS中的另一个重要的概念。从 图解CSS系列 的《CSS 盒模型》和《元素尺寸的设置》中可以得知,Web上的内容都是放置在一个具有一定大小的盒子中。在某些情况下,盒子大小是有限制的,有可能内容会超出这些盒子,在 CSS 中,这种现象被称为 溢出。而且 W3C 的CSS工作小组专门为溢出划为一个独立的模块。在这一章中,将和大家一起探讨 CSS 溢出模块相关的话题。比如,什么是溢出?什么情况下会导致溢出?如何使用CSS来控制溢出等等。

什么是溢出?

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

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

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

CSS设计除了美化UI界面之外,还需要保持 Web 的可阅读性。如果你在 HTML 文档中使用了一个标题(比如<h1>)和一个段落(<p>),在未添加任何 CSS 样式规则情况之下,它们在浏览器中的显示方式是可读的(客户端会给一些标签设置初始样式)。标题大而粗,和段落之间也有一定的间距。然而,你一旦想改变 Web 页面的布局或UI风格,你就开始把一些控制权掌握在自己手中了。在某些情况下,你也需要把处理溢出的工作交给自己。即 要自己处理溢出问题并创造出不那么脆弱的设计

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

CSS 的溢出模块

到目前为止,有关于 CSS 的溢出模块(CSS Overflow Module)主要有两个版本,即 Level 3Level 4 两个版本。我们常见的 overflow 相关特性就是在 Level 3 版本中定义的:

其中“给滚动条保留空间” scrollbar-gutter 属性在 Level 4 有额外的介绍。Level 4 版本给分段溢出中新增了一个伪元素选择器的描述,即 ::nth-fragment()

上图中,带有“笑脸”表情的属性已得到主流浏览器较好的支持。

溢出类型

在 CSS 中的 overflow 主要作用是用来描述一个扩展到该框的(视觉盒子)的边缘,即 内容盒子(content-box)边缘内距盒子(padding-box)边缘边框盒子(border-box)边缘。简单地说,就是如何用来控制溢出,从而不让溢出的内容来影响其他元素的布局。 W3C 的溢出模块定义了两种溢出类型:墨水溢出(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,在其他坐标系统中可能是非矩形。这意味着滚动条有时会在实际上不需要的时候出现。

溢出模块相关属性

CSS溢出模块中 CSS 属性(特性)分为:

  • 用于控制盒子可滚动或剪切的属性:overflowoverflow-xoverflow-y,以及流相关的属性:overflow-blockoverflow-inline,它们分别是 overflow-yoverflow-x 对应的逻辑属性
  • 为滚动条保留空间的属性:scrollbar-gutter
  • 内联轴(溢出省略号)属性text-overflow和块轴溢出属性block-ellipsis,他们被称为“指示溢出”
  • 分段溢出(Fragmenting Overflow)属性:line-clampmax-linescontinue

如果你从未接触过 CSS 逻辑属性,可以移步阅读《图解CSS:CSS逻辑属性》和《CSS的逻辑属性对盒模型带来的变化》!

注意,接下来的内容主要会围绕着滚动与剪切溢出的属性 overflow 来展开!

滚动与剪切溢出

CSS Overflow Module Level 3 规范中我们可以得知,滚动与剪切溢出的相关属性主要有:

其中 overflow 是我们最为熟悉的溢出属性。

在 CSS 中,要使用溢出属性 overflow 来控制溢出内容,我们需要确定我们要应用它的元素具有以下特征:

  • 具有尺寸约束的块级元素,比如使用 widthmax-widthheightmax-height(或对应的逻辑属性 inline-sizemax-inline-sizeblock-sizemax-block-size
  • 或设置white-spacenowrap 的元素

简单地说:

为使 overflow 有效果,块级容器必须有一个指定的高度(height或者max-height,也可以是其对应的逻辑属性)或者将white-space设置为nowrap

注意,在 CSS 中可以不显式设置 widthheight 也可以约束盒子的大小,使用 max-widthmax-height 会比显式使用 widthheight 更友好。它可以让你编写的 CSS 代码更具可防御性

至于为什么使用 max-width 和(或) max-height 可以约束一个盒子大小的具体原因,可以阅读《图解CSS: 元素尺寸的设置》一文。

overflow 属性

overflow 属性是你控制一个元素溢出的方式,它会告诉浏览器你想怎样处理溢出。即,指定了一个盒子的内容是否被剪切到它的内距盒子边缘(padding-box),如果是的话,它是否是一个滚动容器,允许用户将其可滚动溢出区域的剪切部分滚动到滚动容器的视窗中。

滚动容器视觉视窗(可见区域)与它的内距边框盒子边缘(padding-box)重合,被称为滚动视窗

一个元素是否是一个滚动容器,是由 overflow (或他的子属性)的值来决定的。因此,我们先从 overflow 的语法规则开始来深入探讨 CSS 溢出属性:

overflow: [ visible | hidden | clip | scroll | auto ]{1,2}

overflow 属性可接受的值主要有 visiblehiddenclipscrollauto,其中visible 是其初始值。它是 overflow-xoverflow-y 的简写属性,它可以同时接受两个值:

overflow: [overflow-x] [overflow-y]

如果overflow 第二个值未显式设置,则表示 overflow-yoverflow-x 的值相同。例如:

.element {
    overflow: scroll hidden;
}

/* 等同于 */
.element {
    overflow-x: scroll;
    overflow-y: hidden;
}

当然,如果你希望控制一个元素溢出的方式能和书写模式紧密结合在一起,那么你可以使用 overflow-xoverflow-y 对应的 CSS 逻辑属性,即 overflow-inlineoverflow-block。这两个属性都可以分别映射到overflow-xoverflow-y,至于映射到哪个上面,取决于文档的书写模式:

  • 如果文档的书写模式是ltrrtl(即horizontal-tb),那么 overflow-inline映射到overflow-x上,overflow-block映射到overflow-y
  • 如果文档的书写模式是 vertical-lrvertical-rl,那么 overflow-inline 映射到 overflow-y 上,overflow-block 映射到 overflow-x

虽然它们映射的属性取决于文档书写模式,但取值是一样的。

如是你对文档书写模式感兴趣,可以阅读《Web中向左向右》一文。

另外需要注意的是:

  • 设置一个轴为 visible,同时另一个轴为不同的值,会导致设置 visible 的轴的行为变成 auto
  • 即使将 overflow 设置为 hidden,也可以使用 JavaScript 的 Element.scrollTop API来滚动 HTML 元素

overflow属性对应的每个值呈现给用户的效果都是不一样的,我们简单地来看看每个值具体呈现的效果,以及他们之间的差异,这有助于你在控制溢出时,应该选择哪种方式。

我用一张图来向大家展示 overflow 属性取不同值在浏览器的渲染效果:

简单介绍每个值的含义:

  • visible :默认值,内容不会被剪切,溢出容器盒子。容器盒子不是一个滚动容器
  • hidden :内容会被剪切,溢出容器盒子的内容不可见,容器也不会出现滚动条,但可以使用 Element.scrollTop 进行滚动。 容器盒子是一个滚动容器
  • clip :和 hidden 相似,内容会被剪切,溢出容器盒子的内容不可见,也不会出现滚动条,而且使用 Element.scrollTop 也无法进行滚动。此外,与 hidden不同的是,clip 完全禁止通过任何机制进行滚动,因此该容器盒子不是一个滚动容器。另外,与hidden还有一点不同的是,clip 不会导致元素建立一个新的格式化上下文
  • scroll :内容是否溢出容器盒子,容器都会显示滚动条,当内容溢出溢出容器盒子时,溢出的内容不可见,但用户可以滑动滚动条查看溢出容器盒子的内容。容器盒子是一个滚动容器
  • auto :由浏览器定夺,如内容溢出容器,溢出的内容会被剪切,同时容器会出现滚动条,用户可以滑动滚动条查看溢出容器盒子的内容;如果内容未溢出容器,容器不会出现滚动条。它的渲染行为既与scroll相同,又和hidden相同。容器盒子是一个滚动容器

注意,如果你要查看 overflow-inlineoverflow-block 的效果,请使用 Firefox 69+ 浏览器查看。到目前为止,只有 Firefox 69+ 浏览器支持这两个属性!

从上面的示例的展示来看,overflow 取值为 hiddenclip 时,都会对溢出的内容进行剪切,但他们之间有着本质的区别:

  • overflowhidden 值时,容器是一个滚动容器,但取 clip 值时,容器不是一个滚动容器
  • overflowhidden 值时,可以使用 Element.scrollTop 进行滚动,但取 clip 值不行
  • overflowhidden 值时,会创建一个新的格式化上下文,但取 clip 值不会

来看一个 overflow 取值为 hiddenclip 的示例:

.overflow--hidden {
    overflow: hidden;
}

.overflow--clip {
    overflow: clip;
}

点击示例中改变滚动容器的 scrollTop 的值,你会发现设置 overflow:hidden 的容器滚动到了底部,而设置了overflow: clip 的容器没有任何效果:

也就是说 clip 在裁剪溢出内容的同时禁止了容器的滚动!

最后,以下图来结束 overflow 属性的基础介绍:

上图将@bramusTwitter 上发的三张图合并而来

扩展裁剪边界:overflow-clip-margin

overflow-clip-margin 定义了容器盒子的溢出剪裁边缘(Overflow Clip Edge),也就是说,决定了一个元素的溢出部分在被裁剪之前可以超出该元素的盒子多远。这个“多远”的区域被称为 溢出裁剪边缘(Overflow Clip Edge)

简单地说,你可以使用overflow-clip-margin来定义元素在被剪切之前允许在其边界之外画多远。

注意,overflow-clip-margin 对带有 overflow: hiddenoverflow: scroll 的容器盒子不会产生任何影响,因为这些容器盒子没有被定义为使用溢出裁剪边缘。也就是说,overflow-clip-margin 只有在 overflow 显式使用 clip 值才有效。

我们可以按下面语法规则来使用 overflow-clip-margin

overflow-clip-margin: <visual-box> || <length [0,∞]>

该属性可接受两个值:

  • <visual-box> :当指定的偏移量为 0 时(盒子边缘作为溢出裁剪边缘的原点),如果省略,元素的 padding-box 被作为默认值使用。在 CSS 中 <visual-box> 可接受的值为 content-boxpadding-boxborder-box
  • <length [0,∞]> :指定溢出裁剪边缘距元素盒子边框外边缘之间的间距。如果省略,该值被设置为0

注意,overflow-clip-margin 指定溢出裁剪边缘的偏移量只能是正负,因此,该属性取负值则无效

来看一个简单示例:

.overflow {
    --clip-margin: 0;
    overflow: clip;
    overflow-clip-margin: var(--clip-margin);
}

你可以开启示例中的overflow-clip-margin,并且拖动滑动给其设置一个值,你将看到的效果如下:

滚动和溢出

许多溢出行为都会引起滚动条,但有一些特定的滚动行为和属性可以帮助你控制溢出容器上的滚动。

滚动条和布局

overflow取值为scrollauto 时,滚动容器在滚动容器的右侧或底部(ltr书写模式下)会出现滚动条,而这个出现的滚动条会占用一定的预留空间,预留空间位于边框盒子(<border-box>)内边缘和内距盒子(<padding-box>)外边缘之间。然而,为了背景定位区域和背景绘制区域的目的,这个保留空间被认为内距盒子的一部分。

上面示例中,<aside> 元素绝对位置和背景图片都被定位到容器的右上角。

注意,背景图片的起点位置(background-origin) 默认值是 padding-box,从边框盒子(<border-box>)内边缘开始(内距盒子<padding-box>外边缘开始)!

如果<article>上没有滚动条,它们(定位元素和背景图)都会在容器右上角的内距外边缘重合(示例左侧所示);如果有滚动条,那么<aside>右侧与滚动条左侧边缘相稳合,而背景图片内位于滚动条底部(背景图右侧与内距盒子外边缘相稳合):

也就是说,滚动容器的滚动条会占用内距盒子(padding-box)的空间,如果是inline(内联的)而不是overlayed(重叠的),就会争夺空间。即 滚动条预留空间会影响 CSS 盒模型尺寸的计算

正因为,滚动条的存在可能影响盒子尺寸的情况,所以 UA 必须从假设不需要滚动条开始,如果发现需要滚动条,则重新计算盒子尺寸。

注意,滚动条的预留空间影响盒模型的计算只存在于“经典滚动条”中。

经典滚动条和覆盖式滚动条

现在,大家都知道了,滚动条本身的存在是由 overflow 属性控制的。平时关注更多的是容器在什么情况下会出现滚动条以及滚动条的UI效果(比如说,自定义滚动条UI风格),却很少关注滚动条的类型。

事实上,滚动条也有不同的类型,即 经典型滚动条覆盖式滚动条 之分:

上图左侧是覆盖式滚动条,在iOS或Mac系统上很常见,它被放置在内容之上。它们默认不显示,只在用户滚动的时候显示。为了保持滚动条下面的内容可见,它们都是半透明的,但这完全由用户代理(浏览器)来决定。在与它们互动时,其外观(包括大小)会有所不同。

右侧的是经典型滚动条,在Windows系统上很常见,它总是放置在一沟槽中(也称“滚动沟槽”),位于边框盒子(<border-box>)内边缘和内距盒子(<padding-box>)外边缘之间。当出现时占用空间,通常是不透明的,会从相邻的内容中拿走一些空间(改变盒模型大小)。

注意,采用经典型滚动条,滚动条的出现会引起布局变化,产生重排和重绘,对于渲染性能来说是昂贵的

也就是说,滚动条本身的存在是由 CSS 的 overflow 属性决定的,而使用经典型滚动条还是覆盖滚动条则由用户代理(系统或浏览器)决定。同样,用户代理还定义滚动条的外观和大小,以及它们是出现在的开始边缘还是结束边缘。

根滚动器与隐式滚动器

你可能会注意到在移动端或混合应用中的一些滚动器有一个下拉刷新的行为和其他特殊行为。这种行为发生在根容器上。一个页面上永远只有一个根滚动器。默认情况下,documentElement 是页面的根滚动器,然而,通过改变哪个元素是根滚动器,特殊行为可以应用于 documentElement 以外的滚动器,我们把这种新的滚动器称为隐式的根滚动器。

为了创建一个根滚动器,你可以使用一个叫做滚动器提升的东西,即 把一个容器的 position 设置为 fixed(设置为固定定位),并且确保它与视窗一样的尺寸,并在顶部使用滚动条的z-index

上面视频演示的是根滚动器与嵌套的隐式滚动器的效果。

为滚动条保留空间:scrollbar-gutter

大家对于滚动条和滚动器的一些特殊行为有了一个初步的认识之后,我们就可以接着往下阅读。在 CSS 中,有些属性允许你控制滚动条和滚动器的一些特殊行为,让用户有一个更好的体验。

首先我们来看 scrollbar-gutter 属性。

scrollbar-gutter 允许你对滚动沟槽存在的控制,并且与 overflow 属性提供的控制滚动条存在的能力分开。简单地说,scrollbar-gutter属性可以为滚动条提前预留空间,避免页面布局的变化。它的使用很简单:

scrollbar-gutter: auto | stable && both-edges?

此属性的值有以下含义:

auto

overflowscrollauto且有内容溢出时,经典型的滚动条会通过创建滚动沟槽来占用盒子(滚动容器)空间。覆盖式滚动条不会占用盒子空间。

.overflow {
    scrollbar-gutter: auto;
}

.overflow--scroll {
    overflow-y: scroll;
}

.overflow--auto {
    overflow-y: auto;
}

如果你使用的是 macOS 系统,可以将系统中“通用(General)”设置中的“显示滚动条(Show scroll bars)”选项设置为“始终(Always)”,就可以将覆盖式滚动条更换成经典型滚动条。这样就可以在浏览器中查看到 scrollbar-gutter的效果:

stable

overflowhiddenscrollauto时,经典型滚动条会出现滚动沟槽(不管内容是否溢出容器)。覆盖式滚动条不会占用盒子空间。

.overflow {
    scrollbar-gutter: stable
}

从示例效果不难发现,当 scrollbar-gutter 取值为 stable 时,即 overflow 取值为 hidden时,滚动条沟槽(滚动条预留空间)都一直存在;这种现象同样发生在 overflowauto值时且内容未溢出滚动容器。即 scrollbar-gutter取值为stable时,不会改变滚动条本身是否可见,只影响到沟槽的存在

也就是说,在滚动容器上将 scrollbar-gutter 显式设置为stable,可以让UA始终显示滚动沟槽,即使内容没有溢出滚动容器,也没有显示滚动条。这样我们就有了一个视觉上稳定的布局:“当内容开始溢出容器时,滚动条就会显示出来,但不会发生布局移动,因为它会告诉浏览器,要给滚动条预留一定空间(滚动沟槽)”。

另外,当滚动沟槽存在而滚动条不存在时,滚动沟槽的背景会作为 padding 的延伸被绘制出来。

both-edges

both-edgesscrollbar-gutter 取值为 stable 的另一个扩展值。可以通过 both-edges 实现对称性,即 滚动容器两边都有滚动沟槽等同的空间

.overflow {
    scrollbar-gutter: stable both-edges;
}

注意,both-edges 一定要结合 stable 一起使用才生效。它们的结合,可以让滚动容器在视觉上具有对称性,保持美观性。换句话说,如果一个滚动沟槽出现在滚动容器(盒子)的内联轴开始边缘或结束边缘(根据文档的书写模式来决定),另一个滚动沟槽也必须出现在相反的边缘。

上面三个示例,分别向大家演示了scrollbar-gutter取不同值时,UA是怎样给滚动条预留空间的,并且在视觉上又是如何呈现的。最后我们可以用下面这个结合示例来展示 scrollbar-gutteroverflow 的交互作用,显示了在哪些情况下要为经典型滚动条预留空间:

下图阐述经典型滚动条预留空间(滚动沟槽)是否应该存在:

不过,使用 scrollbar-gutter 有两个注意事项:

  • overflow 属性一样,根元素(<html>)上设置的 scrollbar-gutter 会被应用到视窗中
  • overflow 属性不同的是,浏览器不会从 HTML 的 <body> 元素中传播 scrollbar-gutter

丝滑般的滚动:scroll-behavior

上一个 scrollbar-gutter 属性允许你给滚动条预留一定的空间,接下来的这个 scroll-behavior 属性允许你控制滚动容器的一些特殊行为。它可以为一个滚动容器指定滚动行为,其他任何的滚动,例如那些由于用户行为而产生的滚动,不受这个属性的影响。另外,在根元素中指定这个属性时,它反而适用于视窗。

scroll-behavior 的值主要有:

  • auto:滚动框立即滚动,是scroll-behavior 的初始值
  • smooth:滚动框通过一个用户代理预定义的时长、使用预定义的时间函数,来实现平稳的滚动,用户代理应遵循其平台的约定(如果有的话)

scroll-behavior 最常用的地方就是在滚动容器中将其值设置为 smooth,这样会让滚动有一个丝滑般的滑动效果,特别是在一些全屏滑动中常见,比如下面这个示例:

html {
    scroll-behavior: smooth;
}

这个示例,还使用了 CSS 滚动捕捉相关的特性

html {
    --scroll-behavior: auto;
    scroll-behavior: var(--scroll-behavior);
    scroll-snap-type: y mandatory;
}

section {
    scroll-snap-stop: always;
    scroll-snap-align: center;
}

这样滑动或滚动就会有一个更好的体验:

其实在CSS中,除了scroll-behavior和滚动捕捉特性可以控制滚动容器行为之外,还有一些其他属性,比如overscroll-behavior可以控制滚动容器滚动时发生的默认行为,即可以控制浏览器过度滚动时的表现。如果你对可用于改善用户体验相关的滚动特性感兴趣的话,还可以阅读:

指示溢出和分段溢出

在 CSS 的设计中,溢出的内容可以是任何东西,尤其是文本内容,很多时候一些长单词,url地址,非法内容等很容易会产生溢出,严重的还会打破布局。虽然 overflow 可以很好的帮助我们控制溢出内容在浏览器中的表现行为,但在某些情况之下,比如直接剪切溢出内容(overflow取值为hiddenclip),用户体验并不很好。如果需要给用户一个更好的体验,我们应该给用户一个提示,告诉用户还有内容未展示出来。这个提示,我们称之为“指示器”。

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

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

针对这些现象,如果希望给用户更好的体验,往往会在被裁剪的内容最末尾添加一些指定符号,比如三个点,即用指示器告诉用户内容未全部展示,如果需要查阅全部内容需要做一些额外的交互操作(如果有的话)。如今,CSS 就提供相应的特性,分别可以在内联轴和块轴方向为被裁剪的内容提供相应的指示器。根据不同方向,我们也称之为 “指示溢出” 和 “分段溢出”:

特别声明,这里将内联轴方向称为“指示溢出”,块轴方向称之“分段溢出”的说法和W3C的标准规范有一定的差异。之所以这么称,是从视觉呈现的角度出发考虑的。在规范中,内联轴和块轴的溢出统称为“指示溢出”,也分别称为“内联轴溢出”和“块轴溢出”。

接下来,我们要聊的指示溢出对应的是 CSS 的 text-overflow 属性,分段溢出对应的是 line-clamp 属性。

注意,上图是 CSS Overflow Module Level 3的划分,上图中部分属性,比如 block-ellipsis(以前称之为block-overflow)、max-linescontinue 等,到目前为止还未得到主流浏览器支持。因此,我们接下来主要围绕着text-overflowline-clamp 来展开,因为这两个属性在 CSS 中处理文本溢出是非常实用的。

指示溢出:text-overflow

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

text-overflow 可用的值有两个:

  • clip :默认值,表示在内容区域的极限处裁剪文本,因此,有可能裁切半个字符
  • ellipsis :渲染一个省略号字符(U+2026,一般是三个点 ...)来表示被裁剪的文本。这个省略号会被添加到内容区域中,因此会减少显示的文本。如果可用空间小到连省略号都容纳不下时,那么这个省略号也会被裁断

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

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

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

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

注意:溢出内容被裁剪或省略号所处的行的行一侧取决于块的方向,简单地说,取决于文本的阅读模式和书写模式,比如说,如果directionltr,那么被裁剪的内容或省略号位于内联轴的结束边缘,反之trl的话,则位于内联轴的起始边缘。如上面示例所示。

另外一点,文本节点容器是一个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;
}

如何来解这个问题,在稍后介绍溢出实例中,我们接着聊。在这里你只需要知道,text-overflow 要在块级元素上都能生效,即使是匿名盒子,也必须是一个匿名的块级盒子。

CSS中的盒子类型特别地多,不同的上下文格式可以创建不同类型的盒子,如果你想深入了解这方面的话,可以移步阅读《Web布局:display属性》和《Web布局:视觉格式化模型》。

省略号的细节

text-overflow 取值为 ellipsis 时,如果满足相应的条件,在溢出容器的内联轴末端会有省略号(...)的指示符。这里简单了解一下省略号的细节:

  • 省略号只影响渲染,不会影响布局或指针事件。UA 会将省略号上的任何指针事件分配给被省略的元素,就像文本溢出没发生一样
  • 省略号是根据块的样式和基线对齐的
  • 省略号发生在相对定位和其他图形转换之后
  • 如果溢出容器没有足够的空间放置省略号,那么就对省略号本身的渲染进行剪切(在该行的中性字符的同一侧,否则将用 text-overflow: clip进行剪切)
用户与省略号的交互
  • 当用户与内容交互时(比如,编辑,选择,滚动等),UA 可以将 text-overflow: ellipsis 视为 text-overflow: clip
  • 用户选择省略号应该选择省略号的文本。如果所有的省略号都被选中,UA 应该显示省略号的选择。部分选中的省略号文本的行为由 UA 决定

到这里有关于指示溢出就聊完了,我们来看一个示例:

上面这个示例,将影响 text-overflow 效果的其他CSS属性都添加进来了。你可以尝试着改变示例中所列参数(CSS属性)的值来查看浏览器最终呈现的效果。你也可以对其进行交互操作,尤其是text-overflow取值为ellipsis 时,可以检验上面所列的省略号细节和交互操作是否匹配。

最后这个示例是 text-overflow 的扩展用例,展示了 text-overflow 的灵活之处,它和Flexbox布局技术结合在一起,实现了长短内容互换展示的效果:

注意,有关于这个示例实现思路和详细介绍可以参阅《Flexible Overflow》一文。不过,现在可以使用CSS的容器查询关系选择器:has()来实现同样的效果,感兴趣的同学不仿自己一试。

分段溢出:line-clamp

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

CSS Overflow Module Level 3 新增了一个 line-clamp 属性,可以把块容器中的内容限制为指定的行数内,剩余的内容被分割开来,既不被渲染也不被测量。

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

该属性的语法规则很简单:

line-clamp: none | <integer> <'block-ellipsis'>?

line-clamp 还是 max-linesblock-ellipsiscontinue 三个属性的简写属性:

max-lines: none | <integer>
block-ellipsis: none | auto | <string>
continue: auto | discard

如果 line-clamp 取值为 none ,相当于:

  • max-lines 值是 none
  • continue 值是 auto
  • block-ellipsis 值是 none

如果 line-clamp 取值为 <integer>(正整数),相当于:

  • max-lines 值是 <integer>,不带任何单位的整数,不可以是负值;
  • continue 值是 discard
  • block-ellipsis 设置为该值的第二个分量,如果省略则设置为 auto

另外,line-clamp 是早期的 -webkit-line-clamp 升级版,属于标准化的一个属性。如果得到浏览器支持之后,就可以直接使用 line-clamp 属性。不过到目前为止,要使用该功能的话,还只能使用带有 -webkit-前缀的私有属性,即 -webkit-line-clamp

line-clamp一样,-webkit-line-clamp 也是 max-linesblock-ellipsiscontinue 三个属性的简写属性,不同的是:

  • continue 属性取discard值时也需要带上-webkit-前缀,即 -webkit-discard
  • 无条件将block-ellipsis设置为auto

注意, -webkit-discarddiscard 作用相同,只是-webkit-discard 只在父元素的 display 属性的计算值为 -webkit-box-webkit-inline-box 且父元素的 -webkit-box-orient 属性的计算值为vertical 时才生效。

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

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

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

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

虽然-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属性还有一个特性是-webkit-line-clamp不具备的,在line-clamp中还可以省略号之外的字符,比如:

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

它在浏览器中将呈现的效果如下图所示:

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

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

尝试选中示例中的复选框,给溢出元素设置一个padding: 1rem,你看到的效果如下:

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

上面示例,当你选中复选框时,表示溢出容器(元素)显式设置了一个height,并且可以通过滑块来调整height的值。你将看到类似下面这样的效果:

min-height要比height友好一些,只有min-height的值大于限制的行数(line-clamp值)乘以行高(line-height)的值时,才会被截断的内容显示出来:

注意,设置max-height以及其对应的逻辑属性不会产生heightmin-height这样现象

Web设计时不需要的溢出

到这里,有关于 CSS 溢出相关的 CSS 属性(除 max-linescontinueblock-ellipsis之外)就介绍完了。我想你对CSS 的溢出属性有了一个基本的了解。

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

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

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

虽然在介绍 CSS 溢出相关的属性时也用到了不少的示例,但更多的还是趋向于理论的介绍。我想更多的开发者是希望看到 CSS 溢出相关的真实案例(实战),那么接下来的部分,我们就来聊溢出相关的实例。除了介绍实例之外,我们在使用溢出的时候就是会碰到一些问题,所以我们除了介绍实例之外,还会介绍溢出常见的问题,以及又应该如何排查和解决这些问题。如果你对这几面也感兴趣的话,请继续往下阅读。