CSS 文本截断方法总结

虽然文本截断并不是什么很复杂的东西,但浏览器却没有较完善的原生支持。于是,在互联网 Web 开发过程中,诞生了各种奇奇怪怪的解决方案。每个方案都有各自的优缺点和适用场景,请根据自己的需要选择合适的方案。

单行截断

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

white-space: nowrap; white-space 用于指定空白符(white space)的行为。 值 nowrap 会将多个连续空格符或换行符视为一个空格符。默认情况下,文本超过容器宽度时,会自动在合适的地方添加换行符进行换行。设置了该值后,换行效果会被消除,使文本不进行换行,从而实现文本单行显示的效果。此时我们会发现文本发生了溢出:后面的文本内容跑到容器外面去了。

对此,我们再使用 overflow: hidden,将文本进行截断处理,将超出容器显示的文本隐藏起来。如果希望在文本默认处使用省略号,使用 text-overflow: ellipsis; 来指定文本溢出的方式,为文本末追加合适的截断并添加省略号

可以使用使用双引号包裹的字符串值(如 text-overflow: "......")来自定义截断字符,但绝大多数浏览器都不支持(貌似只有 firefox 支持),不推荐使用。

多行截断:-webkit-line-clamp

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

-webkit-line-clamp 可以指定文本在第几行进行截断并添加 。该属性只有在 display 属性设置成 -webkit-box 或者 -webkit-inline-box 并且 -webkit-box-orient 属性设置成 vertical 时才有效果。

随着 Firefox(68 版本开始) 的支持,该方案可以说是目前多行文本截断的不错选择,而且这种写法是官方提供的写法,而不是 CSS trick。

不使用截断字符(如 )的多行截断

.multi-line {
  line-height: 20px;
  max-height: 60px;
  overflow: hidden;
  word-break: break-all;
}

如果你不在意截断字符,上面这种写法就足够了,它会最多保留三行文本。word-break 属性用于指定单词的断行行为。值 break-all 可以保证一个长度超过容器宽度的英文单词(连续的英文字符)能够不溢出容器,而是从中间截断换行。一般来说这个属性是需要添加的,否则会因为一个很长的西欧单词,导致部分内容的缺失。值 break-word 也有类似效果,但它会通过更早的换行来尽量保证英文单词不会被从中间截断。

虽然一般情况来说,不会出现一个英文单词长度超过容器宽度的情况,但测试并不这么认为。

绝对定位法

其实就是通过绝对布局,将容器中的一个子元素放到右下角。我们在上一个方案的基础上做一些修改:

.multi-line {
  position: relative;
  line-height: 20px;
  max-height: 60px;
  overflow: hidden;
  word-break: break-all;
}
.multi-line::before {
  content: "…";
  position: absolute;
  top: 40px;
  right: 0;
  background-color: #fff;
}

伪元素 ::before(当然也可以使用 ::after) 务必要设置和容器元素一样的背景,否则会出现文字重叠的效果。此外,请注意我们使用的是 top: 40px,而不是 bottom: 0。前者对应的位置是第三行的顶部位置,当文本不超过两行的时候,是不会出现该截断字符的;而后者则不管文本有几行都会显示截断字符,这样显然不合适。

但很明显这种写法有一个致命的问题:只要文本超过两行就会显示截断字符,即使文字未能填充满第三行。且可能会将字符从中间截断(可以通过设置透明度背景缓解)。

绝对定位法改良

前面提到的方案,缺点非常明显,但是还是有办法进行改良的,但同样会导致一些其他新的问题。我们先看看 css:

.multi-line-absolute-sub-element-plus {
  position: relative;
  padding-right: 20px;
  line-height: 20px;
  max-height: 60px;
  overflow: hidden;
  word-break: break-all;
}
.multi-line-absolute-sub-element-plus::before {
  content: "…";
  position: absolute;
  bottom: 0; /* 或 top: 40px; */
  right: 0;
  background-color: #fff;
}
.multi-line-absolute-sub-element-plus::after {
  content: "";
  position: absolute;
  right: 0;
  width: 20px;
  height: 20px;
  background-color: #fff;
}

相比上一种方法,该方案通过设置了 padding-right: 20px; 来空出一段右边距,用于放置截断字符。然后除了使用一个放置截断字符的 ::before 元素,我们再补充一个 ::after 元素,相比 ::before,该元素没有指定 top 或 bottom,此时 top 和 bottom 的值就会被浏览器设置为默认的 auto。对于 absolute 定位,此时的垂直距离就会为设为 当前元素如果是 static 定位时的垂直距离,其实就是文本内容的最后一个字符后面。

这样,::after 就能永远位于最后一行文字的右侧。当行数小于或等于最大行数时,::after 会遮挡住 ::before;当行数超过最大行数时则无法遮挡,从而显示出截断字符。

这种方案解决了前一个方案的所有问题,但却有另一个新的问题:容器元素需要额外提供右边距,并让截断字符游离于文本之外,显得有点奇怪。

淡出效果

可以看到,在文本上添加截断字符(如省略号)总有各种各样的问题,或许我们需要换一种思路,设计另外一种样式来表示文本被截断,那就是淡出效果。

.fade-out{
  position: relative;
  line-height: 20px;
  max-height: 60px;
  overflow: hidden;
  word-break: break-all;
}
.fade-out::after {
  content: "";
  position: absolute;
  top: 40px;
  right: 0;
  width: 40%;
  height: 20px;
  background-image: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 70%);
}

淡出效果的好处是在文本超过第二行,但没有充满第三行时,::after 和背景融合在一起,看起来就像不存在一样。这点比上一种方案要好一些。

使用 float

.float {
  --after-width: 30px;
  max-height: 60px;
  overflow: hidden;
}
.float .content {
  float: right;
  margin-left: calc(-1 * var(--after-width)); /* -30px */
  width: 100%;
  line-height: 20px;
  overflow: hidden;
}
.float::before {
  content: "";
  float: left;
  height: 60px;
  width: var(--after-width); /* 30px */
  background-color: blanchedalmond;
}
.float::after {
  content: "…";
  float: right;
  width: var(--after-width); /* 30px */
  height: 20px;
  background-color: #f04;

  /* 偏移操作 */
  position: relative;
  left: 100%; /* 1. 右移容器的宽度距离 */
  transform: translate(-100%, -100%); /* 2. 偏移自身的宽高距离,此时就刚好位于最后一行的最右边 */
}

这个方案很巧妙地利用了 float 的特性。

首先我们忽略掉 ::after 的偏移操作的样式,来看看 ::after 的位置随文本高度的变化。

  • 当文本内容高度小于或等于容器高度时,::after 会出现在 文本的下方最右边;
  • 当文本内容高度大于容器高度(即发生截断)时,::before 和文本内容出现高度高度差,形成了左下方的凹陷,::after 会出现这个 左下角。

当位于左下角时,我们只需要将 ::after 向右偏移 容器宽度-自身宽度 的距离,再像上移动 自身高度 的位置,即可抵达文本容器的右下角。如果文本没有溢出,应用了偏移操作的 ::after 也会跑出文本容器,不会影响样式效果。

该方案的缺点是 只对定高有效,因为 ::before 无法设置 max-height,只能设置 height 导致容器被撑高为固定高度。如果尝试改为 max-height::before 的高度会因为没有文本支撑,高度变为 0,导致 ::after 无法抵达正确的位置。

上面这些都是纯 CSS 的实现,下面我们再看看需要用到 js 的实现。

显示“查看更多”按钮

:hover 可以在鼠标划过元素时应用指定的样式,我们也希望有个名为 :truncated 的伪类能够在文本截断发生时应用指定样式,然而并没有。没有办法,我们只能使用 js 来检测文本是否发生了截断了。

其实思路也很容易想出来,就是比较容器高度和实际文本占据高度,如果文本高度更大,说明发生了截断,否则没有发生截断。

先看一下 CSS 代码:

.read-more {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  word-break: break-all;
  max-width: 600px;
}

对应的 JS 代码如下:

const content = document.querySelector('.read-more')
const btn = document.querySelector('button')
function setReadmoreVisible() {
  if (content.scrollHeight > content.clientHeight) {
    btn.style.display = 'inline-block'
  } else {
    btn.style.display = 'none'
  }
}

window.addEventListener('resize', function()  {
  setReadmoreVisible()
}, false)
window.onload = function() {
  setReadmoreVisible()
}

缺点是每次容器发生变化时,都要手动检测判断高度决定是否显示“查看更多”按钮。当然如果使用 ResizeObserver 来检测容器的改变的话,就不用对每次给可能导致容器变化的事件手动地一个个添加相应函数了,但 ResizeObserver 的实现兼容性比较差。

注:截止 2021 年 5 月,ResizeObserver 的兼容性已经大大提高了,如下图所示:

ResizeObserver 的兼容性(2021年5月)
ResizeObserver 的兼容性(2021年5月)

此外可能会将字符从中间截断,同样可以通过给 ::after 设置透明度渐变的背景缓解。

预设一个最大文本字符长度

我们可以预设一个「加上截断字符后,大致能填满最大一行的的文本的字符数量长度」;然后手动使用 String.prototype.slice 方法对源文本进行截断,并在其后添加截断字符。

因为不同类型字符通常字宽不同,该方案无法保证能尽可能在恰当的位置截断。对于一些不太严格的场景,可以使用这种方案。

本文总结了各种不同的文本截断方法,希望能对你有所启发。

分享