纯 CSS 展开与收起
纯 CSS 的展开与收起可以拆成三招:float 占位钉按钮、checkbox hack 切状态、::after 障眼法藏按钮。三招独立又叠加,前两招做出可用的组件,第三招补上「短文本不显示按钮」这一惯例。
1 · float 占位与文字环绕
展开与收起的按钮应当待在被截断正文的最后一行右端,像「…更多」那样紧跟省略处,正文则从按钮左侧绕过去。难点在于正文有几行、最后一行有多长都是排版之后才知道的,纯 CSS 拿不到这些数字。诀窍是不去算:用一个
float 的零宽占位符把按钮顶到该去的位置。占位符占住「除最后一行外」的高度,随后浮动的按钮便只能落到右下角,正文自然环绕在占位符与按钮的左侧。
1.1 · 占位符高度取 100% 减一行的理由
占位符 float: right 之后,正文从它的左侧与上方开始环绕。把它的高度写成 calc(100% - 一行):收起态容器为
行,占位符即占住前
行的右侧空间;到第
行占位符已结束,而紧跟其后的按钮(同样 float: right 且 clear: both)只能另起浮动行,落点正是第
行的右端。
高度用百分比而非固定值是有讲究的:展开后容器随全文长高,占位符同步变高,把浮动按钮一路推到全文底。于是按钮两态都自动贴底、始终是 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
意味着它不占水平空间、只贡献高度,正文字符照样能排到容器最右。文本不足以填满
行时按钮仍浮在右侧、显得多余,这正是 §3 要解决的问题。
2 · checkbox hack 切换状态
按钮钉好之后要让它真能展开与收起,仍然不写 JS。思路是借 <input type="checkbox"> 充当「是否展开」这个布尔状态:勾选框被视觉隐藏,真正可点的是它的 <label>,也就是那个按钮,点 label 等于切换 checkbox。:checked 伪类随之生效,用兄弟选择器把
max-height 从
行放开,并用按钮 ::after 的 content 把文案从「展开」换成「收起」。
:checked 如何同时驱动 max-height 与按钮文案。2.1 · 三段选择器的分工
| 选择器 | 作用 |
|---|---|
.exp { 视觉隐藏 } |
勾选框不可见,但仍能被 label 切换、仍保留 :checked 态 |
.exp:checked ~ .clamp |
勾选时放开 max-height,正文从
行展到全文
|
.exp:checked ~ .clamp .more::after |
勾选时把按钮文案 content 从「展开」改成「收起」 |
<label for="id"> 通过 id 关联控件,不要求二者在 DOM 上相邻。所以勾选框可以藏在 .clamp 之前以满足 ~ 兄弟选择器,而充当按钮的 label 放在 .clamp 内部、跟着 float 定位到右下角,两者互不干涉。
警示 · max-height 的过渡有个坑:从
行过渡到 none 无法做动画,none 不是可插值的长度。所以展开态要把 max-height 放到一个足够大的具体值(如 100em)而非 none,transition 才能跑。代价是动画时长按这个大值计算,内容越短、动画前段越空走。把动画时长拖到 0 可关掉过渡。
3 · 遮挡层与自动藏按钮
还差一个惯例:文本根本没超出
行时不该出现按钮。JS 方案会去比 scrollHeight 与 clientHeight,纯 CSS 拿不到这两个数。ChokCoco 的解法是在正文末尾放一个绝对定位、铺满整块的同色 ::after 遮挡层,让它替我们判断溢出——不靠测量,靠它自己的位置会不会被文字挤出视口。
3.1 · 遮挡层判溢出的原理
关键在 ::after 的静态位置:绝对定位元素若不写 top 与 left,它会停在「如果不脱离常规流、本该在的位置」,也就是正文流的末尾。再给它 width 与 height 各 100% 铺满整个 .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 终止环绕 —— 本系列用一个零宽浮动占位符把按钮挤到右下角的物理基础。