CSS 与布局 / 纯 CSS 展开与收起 待审核
line-clamp · checkbox hack

纯 CSS 展开与收起

纯 CSS 的展开与收起可以拆成三招:float 占位钉按钮、checkbox hack 切状态、::after 障眼法藏按钮。三招独立又叠加,前两招做出可用的组件,第三招补上「短文本不显示按钮」这一惯例。

1 · float 占位与文字环绕

float · clear · 占位符

展开与收起的按钮应当待在被截断正文的最后一行右端,像「…更多」那样紧跟省略处,正文则从按钮左侧绕过去。难点在于正文有几行、最后一行有多长都是排版之后才知道的,纯 CSS 拿不到这些数字。诀窍是不去算:用一个 float 的零宽占位符把按钮顶到该去的位置。占位符占住「除最后一行外」的高度,随后浮动的按钮便只能落到右下角,正文自然环绕在占位符与按钮的左侧。

图 1-1 · float 零宽占位符把按钮顶到最后一行右端的过程。可打开显形开关,看到这块本应隐形的垫片(演示时临时给了宽度与底色)。

1.1 · 占位符高度取 100% 减一行的理由

占位符 float: right 之后,正文从它的左侧与上方开始环绕。把它的高度写成 calc(100% - 一行):收起态容器为 NN 行,占位符即占住前 N1N-1 行的右侧空间;到第 NN 行占位符已结束,而紧跟其后的按钮(同样 float: rightclear: both)只能另起浮动行,落点正是第 NN 行的右端。

高度用百分比而非固定值是有讲究的:展开后容器随全文长高,占位符同步变高,把浮动按钮一路推到全文底。于是按钮两态都自动贴底、始终是 float,无需切换定位,收起动画也不会跳。

警示 · 这个百分比能不能解析出来,取决于 .clamp 自己是不是 flex item,而不是父级有没有确定高度。实测 Chrome 151:容器 max-height: 60px、行高 20px 时,父级为普通 block 时占位符量得 0px,给父级写死 height: 60px 仍然是 0px,只有父级 display: flex 时才量得 40px,即 100% - 一行(核对于 2026-08)。原因是百分比高度要对着 .clamp 自身解析,而它的高度是被 max-height 约束出来的 auto 值,并非 definite;作为 flex item 时才拿到确定的 cross size。本系列因此把它包在 .cwrap 里。

注 · clear: both 不可省。没有它,按钮会试图挤进占位符旁边的空隙、跟在正文同一行尾;加上之后按钮被迫清除前面的浮动、另开一行,才稳稳落到占位符下方即最后一行的右端。占位符本身是零宽的,width: 0 意味着它不占水平空间、只贡献高度,正文字符照样能排到容器最右。文本不足以填满 NN 行时按钮仍浮在右侧、显得多余,这正是 §3 要解决的问题。

2 · checkbox hack 切换状态

input:checked · label · max-height

按钮钉好之后要让它真能展开与收起,仍然不写 JS。思路是借 <input type="checkbox"> 充当「是否展开」这个布尔状态:勾选框被视觉隐藏,真正可点的是它的 <label>,也就是那个按钮,点 label 等于切换 checkbox。:checked 伪类随之生效,用兄弟选择器把 max-heightNN 行放开,并用按钮 ::aftercontent 把文案从「展开」换成「收起」。

图 2-1 · checkbox hack 的状态切换。可点按钮观察 :checked 如何同时驱动 max-height 与按钮文案。

2.1 · 三段选择器的分工

label 通过 for 关联勾选框,点它即翻转 :checked,这是整个交互唯一的触发器。
选择器 作用
.exp { 视觉隐藏 } 勾选框不可见,但仍能被 label 切换、仍保留 :checked
.exp:checked ~ .clamp 勾选时放开 max-height,正文从 NN 行展到全文
.exp:checked ~ .clamp .more::after 勾选时把按钮文案 content 从「展开」改成「收起」

<label for="id"> 通过 id 关联控件,不要求二者在 DOM 上相邻。所以勾选框可以藏在 .clamp 之前以满足 ~ 兄弟选择器,而充当按钮的 label 放在 .clamp 内部、跟着 float 定位到右下角,两者互不干涉。

警示 · max-height 的过渡有个坑:从 NN 行过渡到 none 无法做动画,none 不是可插值的长度。所以展开态要把 max-height 放到一个足够大的具体值(如 100em)而非 none,transition 才能跑。代价是动画时长按这个大值计算,内容越短、动画前段越空走。把动画时长拖到 0 可关掉过渡。

3 · 遮挡层与自动藏按钮

::after · 绝对定位 · 障眼法

还差一个惯例:文本根本没超出 NN 行时不该出现按钮。JS 方案会去比 scrollHeightclientHeight,纯 CSS 拿不到这两个数。ChokCoco 的解法是在正文末尾放一个绝对定位、铺满整块的同色 ::after 遮挡层,让它替我们判断溢出——不靠测量,靠它自己的位置会不会被文字挤出视口。

图 3-1 · 遮挡层随文本长度变化的位置。可拖动字数越过临界点,观察按钮从被盖住到露出的切换。

3.1 · 遮挡层判溢出的原理

关键在 ::after 的静态位置:绝对定位元素若不写 topleft,它会停在「如果不脱离常规流、本该在的位置」,也就是正文流的末尾。再给它 widthheight100% 铺满整个 .clamp

遮挡层被文字推走的距离恰好编码了「文本有没有溢出」,是一个不需要任何测量的纯几何判断。
情形 ::after 的静态位置 结果
文字短,未截断 紧跟在短文本之后,落在块内靠上处 向下铺满,盖住右下角的按钮
文字长,已截断 被文字顶到块的底部之外 起点已在 overflow 视口之下,按钮露出

原文的推导路径很直观:先在文末放一个 10×10 的红色小块看它落在哪,文字短时它在文末可见,文字一长就被挤下去消失;再把它放大成 100% 见方,确认它确实能罩住整块;最后把红色换成与卡片同色,它就成了隐形的遮挡层。展开态再用 :checked 把它 visibility: hidden 掉,好让全文与「收起」按钮都露出来。

警示 · 遮挡层的颜色必须与底色一致,它靠同色覆盖骗过眼睛,而非真的删除按钮。一旦容器背景不是纯色,如渐变、图片或半透明叠底,这一招就会露馅。这是纯 CSS 方案相对 JS 测量的固有局限。

4 · 三招的分工

三招独立又叠加:前两招做出可用的展开与收起组件,第三招补上短文本不显示按钮这一惯例。
招式 解决什么 关键 CSS
float 占位 按钮钉右下角,文字环绕 父级 .cwrap { display: flex }::before { float: right; height: calc(100% - 一行) },按钮 float: right; clear: both
checkbox hack 无 JS 切展开与收起,换文案 input:checked ~ .clamp { max-height }.more::after { content }
::after 障眼法 文字没溢出时自动藏按钮 ::after { position: absolute; inset: 0; background: 同底色 },靠静态位置被挤出视口

注 · 截断没有用 display: -webkit-box-webkit-line-clamp 能在最后一行渲染省略号,但它把容器变成早期 flexbox 草案的 -webkit-box,其中的 float 与绝对定位 ::after 的静态位置都不再按常规块流工作,而本系列的交互恰好依赖这两者。所以截断改用 max-height 取行高乘行数再配 overflow: hidden,省略号则由按钮的 ::before 显式补一个。原文同时给了 -webkit-line-clamp 版(展开态写 -webkit-line-clamp: 999)与这个兼容版,本系列取后者。

相关链接

  • 纯 CSS 实现多行文本展开收起 · ChokCoco juejin.cn 本系列的源出处:从 line-clamp 截断,到 float 占位定位按钮,再到 ::after 障眼法判断溢出,层层推进的完整思路。
  • 姊妹页:截断与省略 · typesetting playground · typesetting 多行截断本身的细节:text-overflow 单行三件套、-webkit-line-clamp 四件套、标准无前缀 line-clamp(css-overflow-4)的进展。
  • CSS Overflow Module Level 4 · W3C / CSSWG drafts.csswg.org 截断的规范本体:定义标准 line-clamp(及其 longhand max-lines / block-ellipsis / continue)与作为 legacy 兼容语法收编的 -webkit-line-clamp。
  • line-clamp(含 -webkit-line-clamp)· MDN developer.mozilla.org 事实标准的多行截断:必须配齐 display: -webkit-box + -webkit-box-orient: vertical + overflow: hidden,以及为何省略号只在它这套里出现。
  • Can I use · CSS line-clamp caniuse.com -webkit-line-clamp 的实时浏览器支持矩阵:各端覆盖率与前缀依赖,可据此判断截断方案的可用范围。
  • :checked 伪类 · MDN developer.mozilla.org checkbox / radio hack 的根基:用表单控件的勾选态当「全局开关」,配合兄弟 / 后代选择器驱动纯 CSS 交互。
  • float · MDN developer.mozilla.org float 让行内内容环绕、clear 终止环绕 —— 本系列用一个零宽浮动占位符把按钮挤到右下角的物理基础。