图片放容器里被压扁了?z-index 调到 9999 还是被盖?Modal 一打开,滚到底背景页跟着跑?标题断行逼死强迫症,最后一个词孤零零挂在下一行?
这些问题不是你不会写 CSS,是你不知道这几个属性。
1. object-fit: cover --- 图片变形终结者
你肯定写过这种代码:用户头像放在一个 40x40 的圆形容器里,结果用户传了张长方形的图,头像被压成椭圆形。更惨的是列表里的商品图,尺寸五花八门,统一塞进 200x200 的卡片里,有的图胖了、有的图瘦了。
常见的 hack 做法是用 background-image + background-size: cover 代替 <img> 标签,但语义差、SEO 不友好,还不能右键保存。
其实 <img> 标签配 object-fit 就能完美解决:
css
.avatar-img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
html
<img
class="avatar-img"
src="user-uploaded-photo.jpg"
alt="用户头像"
>
object-fit: cover 会保持图片比例,把图片缩放裁剪到刚好填满容器,多余的部分裁掉。就跟 background-size: cover 一个效果,但它是给 <img> 用的。
它还支持其他值:
css
/* 完全填充容器,可能变形 */
.img-fill { object-fit: fill; }
/* 保持比例,完整显示图片,留白 */
.img-contain { object-fit: contain; }
/* 保持比例,裁掉溢出,最常用 */
.img-cover { object-fit: cover; }
/* 保持比例,取图片原始尺寸,也可能溢出 */
.img-none { object-fit: none; }
还有个搭档 object-position,控制从图片哪个位置开始裁剪:
css
.animal-photo {
object-fit: cover;
object-position: top; /* 从顶部开始取,比如拍宠物的照片裁脸不裁身子 */
}
浏览器支持:所有现代浏览器,IE 不支持(谁还在用 IE?)。
一句话:给 <img> 加个 object-fit: cover,这辈子别再为图片变形写 background-image 了。
2. overscroll-behavior: contain --- 锁死滚动,别让背景页跟着跑
打开一个全屏 Modal,用户在里面滚动看内容,滚到底部的时候------咔,背景页面也跟着滚了。更尴尬的是移动端,下拉滚动到顶部时触发了浏览器的"下拉刷新",整个页面重新加载。
这叫"滚动穿透",前端开发绕不过去的坑。
以前的方案很麻烦:打开 Modal 时给 <body> 加 overflow: hidden,关闭时去掉。问题是 iOS Safari 上 overflow: hidden 经常不管用,还得配合 position: fixed 防滑动。代码又多又脆。
其实一行 CSS 就解决了:
css
.modal-body {
overscroll-behavior: contain;
overflow-y: auto;
max-height: 70vh;
}
html
<div class="modal-overlay">
<div class="modal">
<h2>详情</h2>
<div class="modal-body">
<!-- 这下面是可滚动内容,滚到底也不会牵连背景页 -->
<p>长内容...</p>
<p>长内容...</p>
<p>长内容...</p>
</div>
</div>
</div>
overscroll-behavior 有三个值:
css
/* 默认:滚动到边界时,剩余滚动传给祖先元素 */
.scroll-default { overscroll-behavior: auto; }
/* 锁死:滚动到边界就停,不传给任何祖先 */
.scroll-contain { overscroll-behavior: contain; }
/* 完全阻止:不传给祖先。区别于 contain,这个也阻止"回弹"效果 */
.scroll-none { overscroll-behavior: none; }
如果你想阻止移动端的"下拉刷新",在 <body> 上加一行:
css
body {
overscroll-behavior-y: contain;
}
浏览器支持:所有现代浏览器,包括移动端 Safari。IE 不支持(再次提醒:谁还在用 IE?)。
这个属性配上 Modal、抽屉、侧边栏,用户体验提升一个档次。
3. text-wrap: balance --- 标题断行不再逼死强迫症
如果你的页面标题长这样,你就知道这个痛苦:
打造高性能、可扩展的
微服务架构
"微服务架构"四个字孤零零独占一行,上面一行又太长。视觉上严重失衡,设计看到要掀桌。
以前怎么办?手动在标题里插 <br> 标签,一行一行调:
html
<h1>打造高性能、可扩展的<br>微服务架构</h1>
问题是不同屏幕宽度下断行位置不一样,你插好的 <br> 在手机上可能反而更难看。
text-wrap: balance 就是这个问题的标准答案:
css
.page-title {
text-wrap: balance;
max-width: 600px;
}
html
<h1 class="page-title">
打造高性能、可扩展的微服务架构
</h1>
浏览器会自动计算每行字数,让各行的文本量尽量均衡,不会出现最后一行的"孤儿词"。效果和手调 <br> 一样好,而且是响应式的------窗口缩放自动重新计算。
注意一个细节:text-wrap: balance 有性能限制,最多计算 6 行。所以它适合用在标题、副标题这种少量文字的地方,不适合大段正文。
还有个兄弟属性 text-wrap: pretty,解决的是段落末尾的"孤行"问题:
css
.article-content p {
text-wrap: pretty;
}
pretty 会尽量避免段落最后一行只有一个词的情况,适合正文排版。和 balance 的区别是:balance 是"让各行长度均衡"(追求视觉平衡),pretty 是"别让最后一行太可怜"(追求段落美观)。
浏览器支持 :text-wrap: balance Chrome 114+、Edge 114+、Safari 17.5+、Firefox 133+。text-wrap: pretty 目前仅 Chrome/Edge 支持,但 Safari 和 Firefox 正在跟进。balance 已经可以放心用了。
一行 CSS,省掉调试 <br> 位置的时间。
4. isolation: isolate --- z-index 调到 9999 还是被盖?
z-index 应该是 CSS 里被误解最深的属性。很多人遇到"元素被盖住"就往上加数字,10 不行换 100,100 不行换 9999,最后换个 2147483647------还是不管用。
问题不在数字大小,在层叠上下文。
什么是层叠上下文?简单说,当一个元素创建了新的层叠上下文后,它内部所有子元素的 z-index 只在它内部比大小,和外部的元素没交集。这就叫"隔离"。
举个例子:你的页面里有一个 Modal(z-index: 100),Modal 里面又有一个 Dropdown(z-index: 9999)。结果 Modal 旁边的一个 Fixed 按钮(z-index: 200)反而盖住了这个 Dropdown。
为什么会这样?因为 Modal 的父容器可能也设置了 z-index,创建了一个新的层叠上下文。Modal 内部的 Dropdown 无论 z-index 多大,都只能在 Modal 的层叠层级上"往上爬",爬不出这个容器。
isolation: isolate 就是专门来干这事的------有意识地创建层叠上下文,把组件的 z-index 锁在内部。
css
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 100; /* Modal 本身对外只占一层 */
isolation: isolate; /* 内部子元素自己玩,不影响外面 */
}
.modal-dropdown {
position: absolute;
z-index: 10; /* 相对于 Modal 内部,10 就够了 */
}
html
<div class="modal-backdrop">
<div class="modal">
<div class="modal-dropdown">
下拉菜单,z-index: 10 就行,不用 9999
</div>
</div>
</div>
isolation: isolate 的好处是语义清晰。它告诉阅读你代码的人:"这个组件有自己的层叠世界,里面的东西不会跑出来,外面的东西也不会闯进去。" 比 z-index: 0 这种 hack(也能建层叠上下文但意图不明)更可读。
css
/* ❌ 意图不明确:为什么突然设 z-index: 0? */
.card { z-index: 0; }
/* ✅ 意图明确:我在隔离这个组件的层叠上下文 */
.card { isolation: isolate; }
浏览器支持:所有现代浏览器,无一例外。
下次别再加数字了。找到那个没隔离的祖先,加一行 isolation: isolate。
5. :has() --- CSS 终于有"父选择器"了
CSS 选择器一直有个短板:你只能从祖先选到后代,不能反过来。"当前这个 card 里如果包含一张 large-size 的图片,card 的 padding 要变大"------这种需求以前只能靠 JS 给父元素加 class。
:has() 把这个短板补上了。
场景1:父子联动
css
/* 如果 card 里包含图片,padding 加大 */
.card:has(img) {
padding: 24px;
}
/* 如果 card 里包含错误状态,边框变红 */
.card:has(.error-message) {
border-color: red;
}
/* 如果表单里有格式错误的 input,整个 fieldset 高亮 */
fieldset:has(input:invalid) {
background: #fff3f3;
}
html
<!-- 这张卡片有图片,自动加 24px padding -->
<div class="card">
<img src="product.jpg" alt="">
<p>产品描述</p>
</div>
<!-- 这张卡片没图片,走默认样式 -->
<div class="card">
<p>产品描述</p>
</div>
场景2:不用 JS 就能做的交互
:has() 配合表单状态能做很多以前必须用 JS 的事:
css
/* 复选框选中时,隐藏的配置面板显示出来 */
.panel:has(input[type="checkbox"]:checked) .config {
display: block;
}
/* 搜索输入框有内容时,清空按钮显示 */
.search-group:has(input:placeholder-shown) .clear-btn {
display: none;
}
/* 相邻兄弟选择 */
.item:has(+ .item:hover) {
background: #f0f0f0; /* 悬停某一项时,它的前一项也高亮 */
}
html
<label class="panel">
<input type="checkbox"> 开启高级配置
<div class="config">
<!-- 选中后这个区域显示 -->
</div>
</label>
场景3:布局自适配
css
/* 如果 grid 里超过 4 个子项,自动变三列 */
.grid:has(:nth-child(5)) {
grid-template-columns: repeat(3, 1fr);
}
:has() 支持关系选择符,所以还能做兄弟选择、祖先选择:
css
/* 前面有 h2 的 h3,加额外间距 */
h3:has(+ h2) {
margin-top: 2rem;
}
/* 后面跟着 button 的 input,去掉圆角 */
input:has(+ button) {
border-radius: 4px 0 0 4px;
}
浏览器支持:Chrome 105+、Edge 105+、Safari 15.4+、Firefox 121+。全平台能用,放心上生产。
:has() 是这几年来 CSS 最让我兴奋的新特性,它把"CSS 只能向下选"的规则打破了,很多之前非 JS 不可的逻辑现在纯 CSS 就能搞定。
6. @container --- 组件级的响应式
你用 @media 写响应式多少年了?有个问题一直没解决:@media 只能监听 viewport 宽度,管不到组件自己的宽度。
举个例子:同一个产品卡片组件,在首页宽 400px、在侧边栏宽 200px。用 @media 写的响应式只看屏幕宽度,1000px 的屏幕上侧边栏里的卡片照样渲染成大布局------因为 @media 不知道你把它塞进了 200px 的容器。
@container 就是解决这个问题的。
基础用法
第一步,给容器定义一个"查询容器":
css
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
第二步,根据容器宽度写样式:
css
/* 当 sidebar 宽度 < 300px 时,卡片变小 */
@container sidebar (max-width: 300px) {
.card {
display: block; /* 从横排变竖排 */
padding: 12px;
}
.card-title {
font-size: 14px;
}
.card-image {
display: none; /* 太窄了,不显示图片 */
}
}
/* 当 sidebar 宽度 > 500px 时,卡片展开 */
@container sidebar (min-width: 500px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 16px;
}
}
html
<aside class="sidebar">
<!-- sidebar 宽度 200px 时,card 自动变竖排、隐藏图片 -->
<div class="card">
<img class="card-image" src="product.jpg">
<h3 class="card-title">产品名称</h3>
</div>
</aside>
不用名字,用匿名容器
如果你只想让容器宽度影响直接子元素,不需要起名字:
css
.card-wrapper {
container-type: inline-size;
}
/* 匿名容器查询,作用于 .card-wrapper 的直接子元素 */
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
实际场景
页面布局里,同一个组件可能出现三次:全宽区域、两栏区域、侧边栏。以前这三种布局得写三套 CSS,或者用 JS 动态测宽。@container 之后,组件自己根据自己的容器宽度决定怎么渲染,放哪都行。
css
.product-card-grid {
container-type: inline-size;
}
@container (min-width: 350px) {
.product-card {
display: grid;
grid-template-columns: 120px 1fr;
}
.product-card img {
width: 120px;
}
}
@container (max-width: 349px) {
.product-card {
display: block;
}
.product-card img {
width: 100%;
}
}
浏览器支持:Chrome 105+、Edge 105+、Safari 16.0+、Firefox 110+。全线可用。
配合 :has() 使用,你差不多可以告别一大半的 CSS 布局 JS hack 了。
收个尾
六个属性,每个都是一行到几行 CSS 的事,但没听说过之前你可能花半小时在 StackOverflow 上搜各种 workaround。object-fit 终结图片变形,overscroll-behavior 锁死滚动穿透,text-wrap: balance 拯救强迫症断行,isolation: isolate 让人告别 z-index 战争,:has() 和 @container 更进一步------把以前必须靠 JS 干的活搬回了 CSS。
你写过的最离谱的 CSS 问题是什么?有没有那种调了半天最后才发现"其实浏览器早就有原生属性解决这个事"的时刻?留言区等你吐槽。