CSS 布局和自适应设计

  1. 1 基础知识
    1. 应使用哪种布局模式?
    2. 工作原则
  2. 2 Flexbox
  3. 3 网格和子网格
    1. 代码示例:网格和子网格
  4. 4 容器查询
    1. 代码示例:使用容器查询单位的流体排版
  5. 5 原生叠加层、锚点定位和堆叠上下文
  6. 6 溢出跟踪和布局稳定性
  7. 7 视口机制和轨道分布
  8. 8 网格通道(也称为瀑布流)

1 基础知识

尽可能依靠浏览器的布局引擎,以获得更好的性能。在采用硬编码尺寸或复杂的媒体查询之前,请先尝试使用固有尺寸、逻辑属性和 aspect-ratio。

1.1 使用哪种布局模式?

从上到下遍历决策树,并在找到第一个匹配项时停止。请注意,布局可以相互嵌套,每个决策都基于该容器的用例。

  1. 是简单的一行或一列内容吗?使用Flexbox - 一维、内容优先、内容沿单轴分布。
  2. 嵌套元素是否需要与其祖父级网格的轨道对齐?使用 subgrid - 二维,关系优先,继承父级轨道,使孙级元素可以跨同级元素对齐。
  3. 页面或组件结构是否复杂,同时包含行和列?使用网格 - 2D、布局优先,您定义骨架,内容填充骨架。
  4. 内容是否为长篇散文,应拆分为均衡的列?使用多列 - 一维流,报纸样式。
  5. 物品的高度是否各不相同,需要紧密包装?现在,请将 grid 与 grid-auto-flow: dense 搭配使用;仅当原生瀑布流(又称“网格通道”)在您的基准目标中发布时,才使用它(请参阅第 8 节)。
  6. 是否需要让某个元素悬浮在页面上方,并与触发器保持空间上的关联,即使跨越 DOM 边界或堆叠上下文也是如此?使用锚点定位 - 触发器上使用 anchor-name,叠加层上使用 position-anchor(请参阅第 5 节)。

1.2 工作原则

  • 使用逻辑属性(inline-size、block-size、margin-inline、padding-block、inset-inline-start)设置布局尺寸和间距 - 请参阅 CSS:现代架构,了解完整内容。
  • 应用“内容优先”与“布局优先”的心智模型:当内容决定布局流向时,使用 Flexbox;当先定义骨架时,使用 Grid。
  • 使用 place-* 简写形式(place-content、place-items、place-self)在一个声明中沿两个轴对齐。
  • 优先使用内在尺寸(min-content、max-content、fit-content())和弹性轨道(fr、minmax()),而不是固定的 width/height,这样可以减少媒体查询,实现更具弹性的布局。
  • 请勿使用 overflow: hidden 掩盖布局问题,而应从源头解决问题。 例如,某些被替换的元素(如 <canvas>、<img>、<svg>、<video>、<iframe>)默认使用 display: inline,这可能会在某些布局中增加间隙。请勿使用 overflow 剪裁间隙,而是通过应用块级 display-outside 值(例如 display: block、grid、flex 等)来移除间隙。
  • 使用 aspect-ratio 为媒体预留空间,并防止在素材资源加载之前发生布局偏移。
.sidebar       { inline-size: max-content; }    /* Size to longest unbreakable token. */
.main-content  { inline-size: fit-content; }    /* Grow to available space, no further. */
.media         { aspect-ratio: 16 / 9; inline-size: 100%; block-size: auto; }
body.centered  { display: grid; place-content: center; min-block-size: 100dvb; }

如需了解 calc-size() 和具有约束感知功能的固有尺寸,请参阅使用固有尺寸进行计算。

2 Flexbox

一维布局 - 内容沿单个主轴流动,并在交叉轴上对齐。适用于导航栏、工具栏、项目行以及任何单行或单列布局。

正确做法:

  • 使用 display: flex 建立上下文,并使用 flex-direction(row 默认值)设置主轴。
  • 只要有可能发生溢出,就使用 flex-wrap: wrap。不带 overflow: auto/hidden 的 nowrap 会在窄视口中溢出。
  • 在紧凑项(例如标签云、过滤条件胶囊、按钮组或导航链接)的换行行上,在 flex-wrap: wrap 之后叠加 flex-wrap: balance,以便在各行之间均匀分布项,并防止在最后一行出现单个孤立项;不支持的浏览器会回退到 wrap。
  • 在项上使用 flex 简写 <grow> <shrink> <basis>(例如 flex: 1 1 250px),而不是单独设置 flex-grow/flex-shrink/flex-basis。
  • 使用 gap(或 row-gap/column-gap 完整形式)来设置项之间的间距,而不是使用子项外边距。
  • 使用 safe(例如 align-items: safe center)为位置对齐方式添加前缀,这样当容器比其内容窄时,可聚焦的内容就不会被剪裁。
  • 使用 margin-inline-start: auto(或 margin-block-start: auto)将单个内容推送到主轴的最远端,这是标准的逃生舱口。
  • 使用 align-self 替换每个内容的交叉轴对齐。
  • 使用 align-items 可将所有项在交叉轴上居中;在单个项上使用 margin: auto 可将其在两个轴上分别居中;仅当容器换行且行间有额外空间时,才使用 align-content。
  • 在包含长且不可换行内容(网址、代码、长字符串)的 flex 项上设置 min-inline-size: 0(或 min-width: 0),因为默认情况下,flex 项不会缩小到小于其内容大小,从而导致溢出。

请勿:

  • 请勿在 flex 项上使用 justify-self,它仅适用于网格、块和绝对定位布局。请改用自动外边距。
  • 请勿使用 order 或 flex-direction: *-reverse 重新排列互动式内容。它们只会更改视觉顺序,而 DOM 顺序仍会驱动依序焦点,因此键盘 Tab 键流程与用户看到的内容不一致。
  • 请勿将 space-around(两端留出半个间距)与 space-evenly(在之前、之间和之后留出等距)混淆。
  • 别忘了轴翻转:当 flex-direction: column 时,justify-content 会在块轴上对齐,align-items 会在内嵌轴上对齐,这与默认情况相反。
  • 请勿将容器及其子项的大小设置为相互填充,这是导致溢出和意外结果的常见原因。为一侧指定确切尺寸。
  • 请勿在同一项上同时设置 flex-basis 和 width/inline-size,因为在 flex 上下文中,flex-basis 优先,width 会被忽略。使用 flex-basis(或 flex 简写形式)作为调整 flex 项大小的单一事实来源。
.card-grid        { display: flex; flex-flow: row wrap; gap: 1rem; }
.card-item        { flex: 1 1 250px; }                  /* grow, shrink, basis */
.card-item-action { margin-inline-start: auto; }        /* Push to main-axis end. */
.toolbar          { display: flex; align-items: safe center; }
.tag-list         { display: flex; flex-wrap: wrap; flex-wrap: balance; gap: 0.5rem; }

3 网格和子网格

二维布局:明确定义行和列,或让引擎自动推导。Subgrid 可让嵌套网格沿用其父级的轨迹,从而使孙级元素在同级元素之间对齐。

选择网格特征:

  • 您是否确切知道自己需要多少列?
    • 是 - 使用显式轨道(grid-template-columns: 200px 1fr、repeat(3, 1fr) 等)
    • 不同的列是否需要不同的尺寸(边栏 + 主内容,标题跨越所有列)?→ 使用 grid-template-areas 表示已命名的可读区域
    • 所有列是否都统一或纯粹按行号定位?→ 使用 repeat(N, ...) 或命名的线条
    • 否(自适应,商品数量未知)- 使用 repeat(auto-fit, minmax(min, 1fr))
    • 最后一行中的内容是否应拉伸以填充剩余空间?→ auto-fit
    • 最后一行中的空轨道是否应保持其最小尺寸(保留列幽灵槽)?→ auto-fill
  • 您是否需要将商品放置在特定位置?
    • 是 - 使用 grid-column: <start> / <end> 或 grid-area: <name>
    • 否(仅跨多个轨道,流位置无关紧要)- 使用 grid-column: span <n>
  • 子元素是否需要沿用父级网格的轨道大小(同级元素之间不规则边缘对齐)?
    • 是 - 在受影响的轴上使用子网格
    • 每个单元格的子项数量是否可变?→ 子网格仅一个轴;另一个轴使用 grid-auto-rows/grid-auto-columns
    • 子级数量是否固定?→ 两个轴上都使用子网格也没问题
    • 否 - 标准网格,无需子网格

正确做法:

  • 使用 display: grid 建立上下文。
  • 对于复杂的页面级布局,请使用 grid-template-areas。区域名称一目了然,声明可以按行和列对齐,以便于阅读。
  • 使用 repeat(auto-fit, minmax(200px, 1fr)) 可创建自适应卡片网格,拉伸已填充的轨道以填满整行;使用 auto-fill 可将空的重复轨道保持在最小尺寸。
  • 使用 fr 可按比例分配轨道,使用 minmax(min, max) 可创建灵活但有边界的轨道。
  • 使用 grid-column: span <n> 可跨轨道调整大小,使用 grid-column: <start> / <end> 可放置在特定行,使用 grid-area: <name> 可放置在命名区域。
  • 使用子网格(grid-template-columns: subgrid 或 grid-template-rows: subgrid)解决卡片列表中的“边缘参差不齐”问题,使标题、元数据和 CTA 等内部元素在同级元素之间对齐。
  • 将子网格声明与前面的显式 grid-template-rows/-columns 声明配对,作为旧版浏览器的同级级联后备。

请勿:

  • 不要指望 auto-fit/auto-fill 轨道大小来自内容,它来自 repeat() 大小参数。
  • 请勿在互动内容中使用 grid-auto-flow: dense。它能高效地排列内容,但会改变视觉顺序,从而破坏 DOM 顺序的键盘 Tab 键导航流程。
  • 当子元素数量可变时,请勿将子网格应用于两个轴。多余的内容会落在最后一个轨道中;请改用 grid-auto-rows/grid-auto-columns 作为隐式轴。
  • 请勿将 justify-items/align-items(在轨道内对齐项内容)与 justify-content/align-content(在容器内对齐网格轨道)混淆。使用错误的键值对不会产生任何效果,也不会发出任何提示。
  • 请勿在容器上使用 repeat(auto-fit/auto-fill, ...),而不在容器上使用明确的 inline-size。在 display: inline-grid 或未指定大小的 flex 项中,容器没有可划分的宽度,这会导致轨道数量不可预测。

3.1 代码示例:网格和子网格

页面 shell:<main class="page-layout"> 包含 <header>、<aside>、具有 <div class="card"> 个子元素的 <section class="card-grid">,以及 <footer>。

/* Align grid-template-areas in rows and columns for readability. */
.page-layout {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-areas:
    "header  header  header"
    "sidebar main    main"
    "footer  footer  footer";
  gap: 1.5rem;
}

header  { grid-area: header; }
aside   { grid-area: sidebar; }
footer  { grid-area: footer; }

.card-grid {
  grid-area: main;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  grid-template-rows: auto 1fr; /* title block, body block */
  gap: 1rem;
}

.card {
  grid-row: span 2;
  display: grid;
  /* Same-cascade fallback: ignored when subgrid is supported. */
  grid-template-rows: auto 1fr;
  grid-template-rows: subgrid;
}

4 容器查询

查询祖先容器(而非视口)的大小(或计算出的样式)。心智模型:容器查询 = 组件上下文;媒体查询 = 全局页面布局和用户偏好设置(prefers-color-scheme、prefers-reduced-motion)。

正确做法:

  • 在封装容器上使用 container-type: inline-size(仅限宽度查询)或 container-type: size(两个轴)建立包含上下文,然后才能查询其后代。
  • 当嵌套上下文可能发生冲突时,请使用 container-name(或 container 简写形式:container: card / inline-size)为容器命名。
  • 在计算流体类型和间距时,包含容器查询单位:cqi/cqb(逻辑内嵌/块)、cqw/cqh(物理)、cqmin/cqmax。
  • 只要使用 container-type: size,就必须为容器指定明确的 block-size,否则后代元素会折叠,因为尺寸包容会强制容器忽略其内容。

请勿:

  • 请勿将 block-size 用作 container-type 值,因为该值无效。对两个轴都使用 size。
  • 声明 container-type 后,请勿期望子级的固有尺寸会影响容器。一旦启用包含,系统就会将容器计算为没有子项。
  • 请勿依赖不符合条件的祖先元素的后代中的容器查询单位;它们会回退到小视口(svw/svh)。

4.1 代码示例:使用容器查询单位的流体排版

.card-wrapper {
  container: card / inline-size; /* shorthand for container-name / container-type */
}

@container card (inline-size > 400px) {
  .content {
    display: flex;
    gap: 2rem;
  }
}

.title {
  /* Fluid type bound to the container width, not the viewport. */
  font-size: clamp(1rem, 4cqi, 2rem);
}

如需了解组件驱动的响应式样式模式,请参阅尺寸感知样式和平滑扩缩。

5 原生重叠式广告、锚点定位和堆叠上下文

何时使用各种叠加层基元:

  • 使用 popover 实现临时性、非模态界面(弹出式窗口、消息框、提示),这些界面位于顶层,无需 z-index 管理。
  • 将 <dialog> 与 .showModal() 搭配使用,可实现需要焦点捕获和惰性背景的模态互动。
  • 请勿在同一元素上同时使用 popover 和 .showModal(),因为它们是互斥的运行时状态。

锚点定位(叠加层的空间布局):

  • 使用 position-area(或 anchor(),对于边衬区)和 anchor-size(),相对于触发器来定位叠加层并调整其大小。
  • 使用 position-try-fallbacks: flip-block(或 flip-inline),让浏览器在叠加层超出视口时重新定位。
  • 请勿在单个 position-area 值中混用物理关键字和逻辑关键字,请选择一种坐标系。
  • 使用 @supports (anchor-name: --x) 进行功能检测,并提供绝对定位后备方案。

如需了解完整的实现细节、polyfill 策略和 popover 值参考,请参阅声明式对话框和弹出式窗口控件以及位置感知提示。如需了解应用于下拉菜单、弹出式菜单和标签页指示器的锚点定位,请参阅视口感知下拉菜单和锚点定位标签页下划线。

6 溢出跟踪和布局稳定性

以可预测的方式管理布局偏移、滚动条和剪裁。

正确做法:

  • 使用 overflow: auto,以便仅在内容实际溢出时显示滚动条。
  • 使用 overflow: clip 剪裁内容,而无需建立滚动容器;使用 overflow-clip-margin 选择溢出。
  • 使用 scrollbar-gutter: stable 为滚动条预留空间,并防止内容增加时发生布局偏移。
  • 在嵌套滚动区域(例如边栏、模态窗口和列表)上使用 overscroll-behavior: contain(或 none),以阻止滚动链冒泡到页面中;或者在中间滚动封装容器(例如滑动显示轨道)上使用 overscroll-behavior: chain,以便在不触发本地橡皮筋弹跳的情况下,将多余的滚动传播到祖先滚动容器。
  • 使用 -webkit-line-clamp + display: -webkit-box + -webkit-box-orient: vertical 三元组进行多行截断 - 尽管有前缀,但此模式已完全指定,并未弃用。同时声明不带前缀的 line-clamp 简写形式;尚不支持该属性的浏览器会忽略该属性,不会造成任何危害。 请勿:

  • 如果 auto 足够,请勿使用 overflow: scroll,因为即使没有可滚动的内容,scroll 也会强制显示滚动条。

  • 如果您只想剪裁,请勿使用 overflow: hidden,因为 hidden 会建立一个可通过编程方式滚动的滚动容器。

.scrollable-list {
  max-block-size: 400px;
  overflow-y: auto;
  scrollbar-gutter: stable;       /* Reserve scrollbar space. */
  overscroll-behavior: contain;   /* No scroll chaining into the page. */
}

.snippet {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  line-clamp: 3;                  /* Ignored where unsupported. */
  overflow: clip;
}

如需深入了解 overflow: clip 和 overflow-clip-margin,请参阅溢出剪裁控件。如需了解在滑动显示界面中使用 overscroll-behavior: chain 进行嵌套滚动切换,请参阅滑动显示。如需了解滚动条颜色、大小和主题,请参阅自定义滚动条的颜色或粗细、深色模式和使滚动条适应高对比度偏好设置。

7 视口机制和轨道分布

  • 对于必须考虑浏览器界面变化(网址栏收起/展开)的移动版布局容器,请使用 dvh/dvw。
  • 请勿使用 100vw 实现全宽布局,因为它会忽略滚动条宽度并导致水平溢出。请改用 100%、100dvw 或 100svw。

如需了解自适应设计切入点(动态视口单位、为媒体预留空间、自适应排版),请参阅自适应设计。

8 网格通道(也称为瀑布流)

该规范正在开发中,名称为“网格车道”(例如 display: grid-lanes),而早期的实验性原型使用的是 grid-template-rows: masonry。

正确做法:

  • 现在,您可以将网格与 grid-auto-flow: dense 搭配使用,以实现紧密排列,同时接受 DOM 顺序可能与视觉顺序不一致。
  • 如果列表项是文档片段而非权重相同的卡片,请使用多列 (columns: 3; column-gap: 1rem) 布局来呈现内容丰富的瀑布流。
  • 仅将 grid-template-rows: masonry 视为渐进增强,使用 @supports 进行功能检测。

请勿:

  • 在您的基准目标赶上之前,请勿将 grid-template-rows: masonry 作为硬性要求发布。
.gallery       { columns: 3 200px; column-gap: 1rem; }
.gallery > *   { break-inside: avoid; margin-block-end: 1rem; }

@supports (grid-template-rows: masonry) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    grid-template-rows: masonry;
    gap: 1rem;
    columns: unset;
  }
}