CSS 现代特性实战:变量、Grid 与容器查询
Created on
在本博客之前的文章《开发常用 CSS 片段》里,收录了很多解决具体问题的 CSS 代码片段。而这篇文章聚焦另一个维度——CSS 近年来推出的现代特性。这些特性曾经需要 JavaScript 或复杂的 Hack 才能实现,如今一行 CSS 就能搞定。
CSS 自定义属性(CSS Variables)
CSS 自定义属性(Custom Properties)俗称 CSS 变量,是现代 CSS 最基础也最重要的特性之一。
基础用法
/* 在 :root 上定义全局变量 */
:root {
--color-primary: #3b82f6;
--color-secondary: #64748b;
--spacing-base: 16px;
--border-radius: 8px;
--transition: 0.2s ease;
}
.button {
background-color: var(--color-primary);
padding: var(--spacing-base);
border-radius: var(--border-radius);
transition: all var(--transition);
}
/* var() 支持回退值 */
.card {
color: var(--color-text, #333); /* 如果 --color-text 未定义,用 #333 */
}
实现主题切换(深色模式)
CSS 变量的一个杀手级应用是主题切换,无需修改任何 JavaScript:
:root {
--bg: #ffffff;
--text: #1a1a1a;
--border: #e5e7eb;
--card-bg: #f9fafb;
}
/* 系统深色模式自动切换 */
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #f1f5f9;
--border: #1e293b;
--card-bg: #1e293b;
}
}
/* 或者通过 class 手动切换 */
[data-theme="dark"] {
--bg: #0f172a;
--text: #f1f5f9;
--border: #1e293b;
--card-bg: #1e293b;
}
body {
background-color: var(--bg);
color: var(--text);
}
// 一行 JS 切换主题
document.documentElement.setAttribute("data-theme", "dark");
用 JavaScript 动态操作 CSS 变量
// 读取变量
const primary = getComputedStyle(document.documentElement).getPropertyValue(
"--color-primary"
);
// 修改变量(实时生效,影响所有使用该变量的元素)
document.documentElement.style.setProperty("--color-primary", "#ef4444");
// 实战:实现动态主题色
function setThemeColor(hex) {
document.documentElement.style.setProperty("--color-primary", hex);
}
组件级变量(局部作用域)
CSS 变量遵循 CSS 级联规则,可以在组件内部覆盖:
:root {
--button-bg: #3b82f6;
--button-color: white;
}
/* 危险按钮覆盖局部变量 */
.button--danger {
--button-bg: #ef4444;
}
/* button 的样式代码无需修改 */
.button {
background-color: var(--button-bg);
color: var(--button-color);
}
CSS Grid:真正的二维布局
Flexbox 擅长一维布局,CSS Grid 则是专为二维布局设计的,行和列可以同时控制。
基础语法
.grid-container {
display: grid;
/* 定义列:3列,每列1份等宽 */
grid-template-columns: 1fr 1fr 1fr;
/* 简写 */
grid-template-columns: repeat(3, 1fr);
/* 定义行高 */
grid-template-rows: auto 200px auto;
/* 间距 */
gap: 16px;
/* 分别设置 */
column-gap: 24px;
row-gap: 16px;
}
响应式网格(不用媒体查询)
.card-grid {
display: grid;
/* auto-fill 自动填充列,每列最小 280px,最大均分剩余空间 */
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
这一行代码就实现了:
- 宽屏:自动排多列
- 窄屏:自动变成 1 列
- 完全不需要媒体查询
命名区域布局
.layout {
display: grid;
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
grid-template-columns: 240px 1fr 1fr;
grid-template-rows: 60px 1fr 80px;
min-height: 100vh;
}
header {
grid-area: header;
}
aside {
grid-area: sidebar;
}
main {
grid-area: main;
}
footer {
grid-area: footer;
}
布局结构一目了然,比浮动和定位清晰太多。
子元素精确定位
.item {
/* 从第1列线开始,到第3列线结束(跨2列) */
grid-column: 1 / 3;
/* 简写:跨2列 */
grid-column: span 2;
/* 跨2行 */
grid-row: span 2;
}
实战:瀑布流风格的卡片布局
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 10px; /* 极小的行单位,作为基准 */
gap: 16px;
}
/* JS 动态计算每个卡片需要跨多少行 */
function setGridSpan(card) {
const height = card.getBoundingClientRect().height;
const rowSpan = Math.ceil(height / 10); // 10px 是 grid-auto-rows
card.style.gridRowEnd = `span ${rowSpan}`;
}
Container Queries:容器查询
媒体查询(Media Queries)是根据视口宽度调整样式,而容器查询(Container Queries)是根据父元素宽度调整样式。
这解决了一个经典痛点:同一个组件在不同容器里(侧边栏、主内容区)需要不同的样式,但视口宽度却是一样的。
/* 第一步:在父元素上声明容器 */
.card-wrapper {
container-type: inline-size;
container-name: card; /* 可选,有名字更精确 */
}
/* 第二步:在子元素里写容器查询规则 */
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
.card__image {
width: 160px;
flex-shrink: 0;
}
}
@container card (max-width: 399px) {
.card {
display: block;
}
.card__image {
width: 100%;
height: 200px;
object-fit: cover;
}
}
<!-- 窄容器(侧边栏):卡片显示为垂直布局 -->
<aside style="width: 300px">
<div class="card-wrapper">
<div class="card">...</div>
</div>
</aside>
<!-- 宽容器(主内容区):卡片显示为水平布局 -->
<main style="width: 800px">
<div class="card-wrapper">
<div class="card">...</div>
</div>
</main>
浏览器支持:Chrome 105+,Safari 16+,Firefox 110+,已经可以在生产环境使用。
:has() 选择器:父元素选择器
:has() 是 CSS 的”父元素选择器”,可以根据子元素的状态选中父元素,这在以前只能靠 JavaScript 实现。
/* 选中"包含 img 子元素的"card */
.card:has(img) {
padding: 0; /* 有图片的卡片不需要内边距 */
}
/* 选中"包含选中 checkbox 的"表单项 */
.form-item:has(input:checked) {
background-color: #f0fdf4;
border-color: #86efac;
}
/* 选中"包含空 input"的标签(表单验证提示)*/
.field:has(input:placeholder-shown) .label {
color: #9ca3af;
}
/* 导航菜单:子菜单展开时高亮父项 */
.nav-item:has(.submenu:hover) > .nav-link {
color: var(--color-primary);
}
实战:根据子元素数量调整布局
/* 1个子元素:全宽 */
.grid:has(> :nth-child(1):last-child) {
grid-template-columns: 1fr;
}
/* 2个子元素:各半 */
.grid:has(> :nth-child(2):last-child) {
grid-template-columns: 1fr 1fr;
}
/* 3个或更多:三列 */
.grid:has(> :nth-child(3)) {
grid-template-columns: repeat(3, 1fr);
}
这种纯 CSS 的弹性布局,以前需要 JavaScript 动态计算并添加类名。
综合实战:响应式卡片组件
把以上特性综合起来,写一个完全现代化的卡片组件:
/* CSS 变量:统一管理设计 token */
:root {
--card-radius: 12px;
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--card-padding: 20px;
--card-gap: 16px;
}
/* Grid 布局:自适应列数 */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--card-gap);
container-type: inline-size;
container-name: card-grid;
}
/* 卡片基础样式 */
.card {
border-radius: var(--card-radius);
box-shadow: var(--card-shadow);
overflow: hidden;
container-type: inline-size;
container-name: card;
}
/* 容器查询:卡片在宽容器中横向排列 */
@container card (min-width: 360px) {
.card__body {
display: flex;
align-items: center;
gap: 16px;
}
.card__image {
width: 120px;
height: 120px;
flex-shrink: 0;
}
}
/* :has():有图片的卡片特殊处理 */
.card:has(.card__image) {
padding: 0;
}
.card:not(:has(.card__image)) {
padding: var(--card-padding);
}
/* 深色模式:只改变量,不改组件代码 */
@media (prefers-color-scheme: dark) {
:root {
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
}
总结
| 特性 | 解决的问题 | 浏览器支持 |
|---|---|---|
| CSS 变量 | 避免重复值、实现主题切换 | 全面支持(IE 除外) |
| CSS Grid | 复杂二维布局 | 全面支持 |
| Container Queries | 组件根据容器响应式 | Chrome 105+,Safari 16+ |
:has() | 父元素/条件选择 | Chrome 105+,Safari 15.4+ |
这些特性让 CSS 真正具备了”组件化”的能力——样式可以根据上下文自适应,不再依赖 JavaScript 干预。建议在新项目中大胆采用,用 @supports 做必要的降级处理:
@supports (container-type: inline-size) {
/* 支持容器查询时的样式 */
}