纯 CSS 展开 / 收起:三招拆解
下面把纯 CSS 的展开 / 收起拆成三招,每招一块可拖参数的实时演示,旁边同步给出生效的 CSS。三招层层递进:float 占位钉按钮 → checkbox hack 切状态 → ::after 障眼法藏按钮。
1 · 把按钮钉在右下角:float 占位 + 文字环绕
展开 / 收起的按钮应当待在被截断正文的最后一行右端,像「…更多」那样紧跟省略处;正文要从按钮左侧绕过去。难点是:正文有几行、最后一行有多长,都是排版后才知道的,纯 CSS 拿不到这些数字。诀窍是不去算,而是用一个
float 的零宽占位符把按钮「顶」到该去的位置——占位符占住「除最后一行外」的高度,随后浮动的按钮便只能落到右下角,正文则自然环绕在占位符与按钮的左侧。
1.1 · 占位符的高度为什么用 100% − 一行
占位符 float: right 后,正文会从它的左侧、上方开始环绕。把它的高度写成 calc(100% - 一行):收起态容器为 N 行,占位符即前 N−1 行,占住前 N−1 行的右侧空间;到第 N 行占位符已结束,但紧跟其后的按钮(同样 float: right 且 clear: both)只能另起浮动行——落点正是第 N
行的右端。关键在于高度用百分比而非固定值:展开后容器随全文长高,占位符同步变高,把浮动按钮一路推到全文底——于是按钮两态都自动贴底、始终是 float、无需切换定位,收起动画也不会跳。代价是百分比高度要求父级有确定高度,而
max-height 截断的容器高度并非确定值,需把它放进一个 display: flex 的父级(本系列的 .cwrap),flex item 才会让这个百分比解析生效。
clear: both 是关键。 没有它,按钮会试图挤进占位符旁边的空隙、跟在正文同一行尾;加上 clear: both,按钮被迫清除前面的浮动、另开一行,这才稳稳落到占位符下方(即最后一行)的右端。
占位符是零宽的。 width: 0 意味着它不占据水平空间,只贡献高度——正文的字符照样能排到容器最右,不会平白少一块。它纯粹是个「高度垫片」,用来把按钮的浮动起点推到最后一行。把上面的「显形」打开,能看到这块本应隐形的垫片(演示时临时给了宽度和底色)。文本不足以填满 N
行时,按钮此时仍浮在右侧、显得多余——这正是 ::after 障眼法要解决的问题。
2 · 纯 CSS 切状态:checkbox hack 展开 / 收起
float 占位把按钮钉好了,现在让它真的能展开 / 收起——仍然不写 JS。思路是借 <input type="checkbox"> 充当「是否展开」这个布尔状态:勾选框被视觉隐藏,真正可点的是它的 <label>(就是那个按钮),点 label 等于切换 checkbox。:checked
伪类随之生效,用兄弟选择器 .exp:checked ~ .clamp 把 max-height 从「N 行」放开,并用按钮 ::after 的 content 把文案从「展开」换成「收起」。
2.1 · 三段选择器,各管一件事
| 选择器 | 作用 |
|---|---|
.exp { 视觉隐藏 } |
勾选框不可见,但仍能被 label 切换、仍保留 :checked 态 |
.exp:checked ~ .clamp |
勾选时放开 max-height,正文从 N 行展到全文 |
.exp:checked ~ .clamp .more::after |
勾选时把按钮文案 content 从「展开」改成「收起」 |
为什么 label 可以放在任意位置? <label for="id"> 通过 id 关联控件,不要求二者在 DOM 上相邻。所以勾选框可以藏在 .clamp 之前(满足 ~ 兄弟选择器),而充当按钮的 label 放在 .clamp 内部、跟着 float 定位到右下角,两者互不干涉。
max-height 的 transition 有个坑。 从「N 行」过渡到 none 是无法做动画的(none 不是可插值的长度)。所以展开态把 max-height 放到一个足够大的具体值(如 100em)而非 none,transition
才能跑;代价是动画时长会按这个大值计算,内容越短、动画前段越「空走」。把动画时长拖到 0 可关掉过渡。
3 · 障眼法:文字够短就自动藏起按钮
前两步做出了能展开 / 收起的按钮,但还差一个惯例:文本根本没超出 N 行时,不该出现按钮。JS 方案会去比 scrollHeight > clientHeight;纯 CSS 拿不到这两个数。ChokCoco 的解法是个障眼法:在正文末尾放一个绝对定位、铺满整块的白色 ::after 遮挡层,让它替我们「判断」溢出——不靠测量,靠它自己的位置会不会被文字挤出视口。
3.1 · 为什么这个遮挡层能「判断」溢出
关键在 ::after 的静态位置:绝对定位元素若不写 top / left,它会停在「如果不脱离常规流、本该在的位置」,也就是正文流的末尾。再给它 width / height: 100% 铺满整个 .clamp。于是:
| 情形 | ::after 的静态位置 |
结果 |
|---|---|---|
| 文字短(未截断) | 紧跟在短文本之后,落在块内靠上处 | 向下铺满 → 盖住右下角的按钮 |
| 文字长(已截断) | 被文字顶到块的底部之外 | 起点已在 overflow 视口之下 → 按钮露出 |
3.2 · 从一个小红块到隐形白块
原文的推导很直观:先在文末放一个10×10 的红色小块看它落在哪——文字短时它在文末可见,文字一长就「被挤下去」消失。把它放大成
100% × 100% 的红块,确认它确实能罩住整块;最后把红色换成与卡片同色的白色,它就成了隐形的遮挡层——文字短时悄悄盖住按钮,文字长时自己缩到视口外。展开态再用 :checked 把它 visibility: hidden 掉,好让全文和「收起」按钮都露出来。
三招合体即完整组件。 float 占位(钉按钮)+ checkbox hack(切状态)+ ::after 障眼法(判溢出),叠起来就是一个零 JS、可展开 / 收起、且短文本自动隐藏按钮的多行文本组件。把文本字数拖过临界点,再点展开,体会三者如何协同。
遮挡层的颜色必须与底色一致。 它是靠「同色覆盖」骗过眼睛,而非真的删除按钮。所以一旦容器背景不是纯色(渐变、图片、半透明叠底),这一招就会露馅——这也是纯 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:#fff } 靠静态位置被挤出视口 |
为什么不用 display: -webkit-box 做截断? -webkit-line-clamp 能在最后一行渲染省略号,但它把容器变成早期 flexbox 草案的 -webkit-box,其中的 float 与绝对定位
::after 的静态位置都不再按常规块流工作。本系列的交互依赖这两者,所以截断改用
+ overflow: hidden。原文同时给了两个版本:-webkit-line-clamp 版(展开态 -webkit-line-clamp: 999)与这个 max-height 兼容版,本系列取后者;省略号则由按钮的 ::before 显式补一个
。
相关链接
- 纯 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。
- -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 终止环绕 —— 本系列用一个零宽浮动占位符把按钮挤到右下角的物理基础。