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;
}

这一行代码就实现了:

命名区域布局

.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); // 10pxgrid-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) {
  /* 支持容器查询时的样式 */
}