每天一个高级前端知识 - Day 5
今日主题:现代CSS架构 - Container Queries、:has()、CSS嵌套彻底重塑组件样式
核心概念:CSS终于有了真正的逻辑作用域
长期以来,CSS缺少响应组件自身尺寸的能力,只能依赖@media(视口)。现代CSS三剑客彻底改变了这一局面。
🔍 Container Queries(容器查询)
解决的核心问题:组件在不同父容器中自适应样式,而非仅依据视口。
/* 定义容器 */
.card-container {
container-type: inline-size;
container-name: card;
}
/* 查询容器宽度 */
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
.card img {
width: 40%;
}
}
@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}
实战:自适应卡片组件
<div class="sidebar">
<div class="card-container">
<div class="card">
<img src="avatar.jpg" />
<h3>用户资料</h3>
<p>侧边栏中的紧凑卡片</p>
</div>
</div>
</div>
<div class="main-content">
<div class="card-container">
<div class="card">
<img src="avatar.jpg" />
<h3>用户资料</h3>
<p>主区域中的完整卡片</p>
</div>
</div>
</div>
🎭 :has() 父级选择器
解决的核心问题:终于可以根据子元素状态影响父元素!
/* 包含错误输入的字段组显示红色边框 */
.form-group:has(input:invalid) {
border: 2px solid red;
background-color: #fff0f0;
}
/* 包含图标的按钮增加内边距 */
button:has(svg) {
padding: 12px 16px;
display: inline-flex;
gap: 8px;
}
/* 有缩略图的文章卡片使用网格布局 */
.article-card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
gap: 16px;
}
/* 任何包含选中复选框的列表项高亮 */
li:has(input[type="checkbox"]:checked) {
background-color: #e8f5e9;
text-decoration: line-through;
}
📦 CSS嵌套(告别Sass)
/* 原生CSS嵌套 - 不再需要Sass! */
.card {
padding: 1rem;
/* 直接嵌套 */
.card-header {
font-weight: bold;
/* & 引用父选择器 */
&:hover {
background-color: #f0f0f0;
}
}
/* 媒体查询嵌套 */
@media (min-width: 768px) {
padding: 2rem;
}
/* & 可以放在任意位置 */
body.dark-theme & {
background-color: #1a1a1a;
}
}
🚀 完整实战:响应式仪表盘组件
/* 仪表盘网格 - 使用容器查询实现自适应 */
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
container-type: inline-size;
container-name: dashboard;
}
/* 统计卡片基类 */
.stats-card {
container-type: inline-size;
container-name: stats;
padding: 1rem;
border-radius: 0.75rem;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
/* 默认:垂直布局 */
.stats-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* 当卡片宽度超过200px时切换为水平 */
@container stats (min-width: 200px) {
.stats-content {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
}
/* 使用:has()实现智能高亮 - 悬停或包含特定数值 */
.stats-card:has(.value[data-status="warning"]) {
border-left: 4px solid orange;
}
.stats-card:has(.value[data-status="critical"]):hover {
background-color: #ffebee;
transform: scale(1.02);
transition: all 0.2s;
}
.stats-card:not(:has(.value)) {
opacity: 0.6;
filter: grayscale(0.3);
}
/* CSS嵌套 + 容器查询的组合 */
.dashboard {
@container dashboard (min-width: 800px) {
grid-template-columns: repeat(4, 1fr);
.stats-card:first-child {
grid-column: span 2;
/* 嵌套内再嵌套 */
@container stats (min-width: 300px) {
.stats-content {
flex-direction: row;
.value {
font-size: 3rem;
}
}
}
}
}
}
💡 高级技巧:CSS作用域与样式隔离
/* @scope 规则 - 限制选择器的作用域 */
@scope (.component) to (.ignore) {
/* 只在.component内生效,且不穿透.ignore */
p {
margin: 1em 0;
}
/* & 表示作用域根元素 */
& > h2 {
font-size: 1.5rem;
}
}
/* 层级叠层 - 定义不同层的样式权重 */
@layer reset, theme, components, utilities;
@layer reset {
* { margin: 0; padding: 0; box-sizing: border-box; }
}
@layer theme {
:root { --primary: #0066cc; }
}
@layer components {
.button {
background: var(--primary);
/* ... */
}
}
@layer utilities {
.flex { display: flex; }
/* utilities 优先级最高 */
}
🎯 今日挑战
实现一个完全自适应的产品卡片网格,要求:
- 使用 Container Queries 让卡片根据自身容器宽度切换3种布局(紧凑/标准/详细)
- 使用
:has()实现:卡片包含视频时显示播放图标,包含折扣标记时显示红色价格 - 使用原生 CSS 嵌套组织所有样式(不依赖预处理器)
- 实现暗色主题切换,同时使用容器查询保持自适应
参考实现(点击展开)
/* 核心实现思路 */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
container-type: inline-size;
/* 暗色主题支持 */
@media (prefers-color-scheme: dark) {
background: #1a1a1a;
}
}
.product-card {
container-type: inline-size;
container-name: product;
border-radius: 0.75rem;
overflow: hidden;
transition: all 0.2s;
/* 嵌套组织样式 */
.card-content {
padding: 1rem;
.title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.price {
color: #0066cc;
font-weight: 700;
}
}
/* 紧凑布局 (< 200px) */
@container product (max-width: 199px) {
.card-content {
.title { font-size: 0.875rem; }
.description { display: none; }
.price { font-size: 0.875rem; }
}
}
/* 标准布局 (200px - 350px) */
@container product (min-width: 200px) and (max-width: 349px) {
.card-content {
.title { font-size: 1rem; }
.description {
display: -webkit-box;
-webkit-line-clamp: 2;
}
}
}
/* 详细布局 (≥ 350px) */
@container product (min-width: 350px) {
display: flex;
gap: 1rem;
.card-content {
flex: 1;
.description {
display: block;
margin: 0.5rem 0;
}
}
}
/* :has() 魔法 - 视频标记 */
&:has(.media-video)::before {
content: "🎬";
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(0,0,0,0.7);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
/* 折扣标记 */
&:has(.badge-discount) .price {
color: #dc2626;
&::after {
content: " 🔥";
}
}
/* 缺货暗淡效果 */
&:has(.badge-out-of-stock) {
opacity: 0.5;
filter: grayscale(0.3);
}
}
📊 浏览器兼容性 (2025)
| 特性 | Chrome | Firefox | Safari |
|---|---|---|---|
| Container Queries | 105+ | 110+ | 16+ |
| :has() | 105+ | 121+ | 15.4+ |
| CSS Nesting | 112+ | 117+ | 16.5+ |
| @scope | 118+ | 不支持 | 17.4+ |
明日预告:Web Components 4.0 - 跨框架组件开发的终极方案(包括声明式 Shadow DOM、CSS 作用域增强)
💡 今日金句:现代CSS让组件真正拥有了"自我意识",不再被动依赖全局视口!