布局实验室 Layout Lab
本章目录 / 案例进度

第 3 章 · 现代布局技术

Flexbox 与 Grid:分工、原理与选型

这两个模块不是竞争关系,而是分工关系。理解「一维 / 二维」这一条分界线,你就能在写第一行代码之前决定用谁 —— 而不是先写 Flex,发现不行再换成 Grid。

4 个知识点 4 个可交互演示 约 30 分钟

案例第 3 阶段:把骨架写成代码。骨架方案定了:桌面三栏(左归档 / 中正文 / 右信息)、平板两栏、手机单栏。本章的任务是选对工具 —— 页面骨架用 Grid,卡片内部用 Flex,并让这套分工在整份代码里保持一致。

3.1

Flexbox:一条主轴上的分配问题

Flex 只解决一件事:让一组项目在一条轴上排布,并决定剩余空间怎么分。

讲解

主轴与交叉轴

Flex 容器里有两个轴:主轴(main axis,由 flex-direction 决定)与交叉轴(cross axis,垂直于主轴)。所有属性都只作用在其中一个轴上,把这条记牢,属性名就不再需要死记:

  • justify-content → 主轴上的分布(项目之间和两端怎么分空间)
  • align-items → 交叉轴上的对齐(单行内每个项目怎么对齐)
  • align-content → 交叉轴上「行之间」的分布(只在换行且有多行时有效)

方向改变时,两个轴的角色会互换 —— flex-direction: column 之后,justify-content 管的是垂直方向。这也是 Flex 最常被误用的地方:以为属性写错了,其实是轴换了。

容器决定「怎么排」,项目决定「占多少」

项目侧最核心的是三个属性,它们共同回答「这一项占用多少空间」:

  • flex-grow:有剩余空间时,按比例分给谁(默认 0,即不参与分配)
  • flex-shrink:空间不够时,谁可以被压缩(默认 1)
  • flex-basis:分配前的基础尺寸(默认 auto,即由内容决定)

日常用得最多的是简写 flex: 1 1 0(等分剩余空间)与 flex: 0 0 200px(固定 200px 不接受压缩)。注意 flex: 1 展开后是 1 1 0%,其中的 basis: 0 意味着「忽略内容宽度,从零开始等分」—— 这是 flex: 1flex: auto(= 1 1 auto,内容宽的项会更宽)的关键差别。

两个高频陷阱。一是 min-width: auto:Flex 项目默认不会缩到比内容更窄,因此一段长文本或长代码会把容器撑破 —— 给它加 min-width: 0 即可。二是嵌套过深:用 Flex 套 Flex 去模拟二维表格,代码会迅速失控,那正是 Grid 的活。

Flex 的强项
导航条、按钮组、表单行、卡片内部(图 + 文)、底部对齐的卡片页脚 —— 凡是「一排东西」都是 Flex 的主场。
Flex 的边界
它不知道「列」的存在:多个容器之间无法共享轨道,因此跨卡片对齐标题、跨行对齐高度都要靠别的办法。

关键代码

卡片内部:一个典型的 Flex 场景 css
.post-card {
    display: flex;
    flex-direction: column;      /* 主轴改为垂直:卡片上下堆叠 */
    gap: 12px;
    min-width: 0;                /* 允许被压缩,防止长内容撑破容器 */
  }

  .post-card__meta {
    display: flex;
    align-items: center;         /* 交叉轴(垂直)居中 */
    justify-content: space-between;  /* 主轴(水平)两端分布 */
    gap: 8px;
  }

  .post-card__footer {
    margin-top: auto;            /* auto margin 吸走剩余空间 → 页脚被压到底部 */
  }

  .post-card__title {
    flex: 1 1 0;                 /* grow shrink basis:等分可用宽度 */
    min-width: 0;                /* 配合 overflow:hidden 才能让长标题出现省略号 */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  
  • margin-top: auto 是 Flex 里最实用的技巧之一:它把「剩下的空间」全吃掉,于是页脚被顶到底部,而不需要 position: absolute 或固定高度。
  • 注意 min-width: 0 出现了两次:一次在容器上(允许自己被压缩),一次在标题上(允许省略号生效)。这是 Flex 最常见的一类「玄学问题」的真正原因。
  • gap 而不是 margin-right:不必处理最后一个元素,间距也不会在压缩时被吃掉。

动态演示

flex-direction
flex-wrap
justify-content(主轴)
align-items(交叉轴 · 单行)
align-content(交叉轴 · 多行)
项目数量
辅助
让某一项 flex-grow: 1(检查它是否吸收剩余空间)

实时输出:上面每一步操作对应的 CSS css

推荐路径:先把 flex-wrap 改成 wrap 并把容器高度拉到 300,然后逐个试 align-content —— 你会看到「单行对齐」和「多行分布」的差别。再切到 column,观察两个轴的角色互换、以及轴标签的移动。

3.2

Grid:先画轨道,再放内容

Grid 的工作方式与 Flex 相反:先在容器上定义好行列轨道,再把项目放进格子里。

讲解

轨道、单元格、区域

Grid 把容器切成轨道(track,即行与列),轨道交叉形成单元格,相邻单元格可以合成区域。项目通过三种方式落位:自动放置(默认,按顺序填)、线号定位(grid-column: 2 / 4)、命名区域(grid-area: main)。

关键词是「先声明,后放置」。这让你在写内容之前就先描述版式,也正是它能替代大量断点与嵌套的原因。

fr 与 minmax:弹性轨道的两种写法

  • 1fr 表示「剩余空间的一份」。1fr 2fr 1fr 就是三列按 1:2:1 分配剩余空间。
  • minmax(0, 1fr)1fr 的差别很关键:轨道的最小值是 auto 时,内容(长单词、长代码、表格)会把轨道撑得比 1fr 更宽,导致整个网格溢出。只要列里有不可预测的长内容,就写 minmax(0, 1fr)
  • minmax(200px, 1fr) 混用固定与弹性:侧栏恒为 200px,主内容吃掉剩下的。minmax(0, 220px) 则让侧栏「最多 220px,窄了就缩」。

auto-fill / auto-fit:不写媒体查询的自适应

repeat(auto-fill, minmax(220px, 1fr)) 是 Grid 最有价值的一行代码:容器变宽就自动多塞几列,变窄就自动减少列数,完全不需要媒体查询。它有两点需要注意:

  • auto-fill 会保留空轨道,auto-fit 会把空轨道折叠掉。元素少、希望它们「撑满一行」时用 auto-fit;希望保持与别处一致的列宽节奏时用 auto-fill
  • minmax() 里的最小值决定了「多少像素开始换列」—— 它就是断点的替代物,只是由浏览器替你算。

显式网格与隐式网格。你声明的行叫显式网格;项目超出的部分会由浏览器自动生成隐式网格来容纳。grid-auto-rows: minmax(120px, auto) 可以控制这些自动行的最小高度,是「卡片墙」保持等高节奏的常用手法。

关键代码

三种轨道写法,三种适用场景 css
/* 1. 等分:三列同宽 */
  .tracks--even {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 16px;
  }

  /* 2. 固定 + 弹性:侧栏恒宽,主内容吃掉剩余空间 */
  .tracks--sidebar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 240px;
    gap: 24px;
  }

  /* 3. 自适应卡片墙:不写媒体查询,容器变宽自动加列 */
  .tracks--auto {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 16px;
    grid-auto-rows: minmax(120px, auto);   /* 隐式行也有最小高度 */
  }

  /* 显式放置:让某一项跨列、跨行 */
  .featured {
    grid-column: 1 / -1;                   /* 从第 1 条线到最后一 条线 = 通栏 */
    grid-row: span 2;                      /* 占两行,通常用于「一主多辅」的卡片墙 */
  }
  
  • 1 / -1 里的 -1 表示「最后一条列线」,因此它天然是「通栏」,无论栅格有多少列 —— 比写 1 / 13 更安全。
  • 自适应卡片墙的代价是列宽不完全可控(有 ±220px 内的浮动),做像素级对齐的版面时仍然要用断点或命名区域。
  • 本站的演示区、卡片阵列、货架式布局大量使用了 repeat(auto-fit, minmax(...)):它是「少写断点」的主要手段。

动态演示

grid-template-columns
grid-template-rows
项目数量
justify-items(格内水平)
align-items(格内垂直)

实时输出:上面每一步操作对应的 CSS css

试三次:① 切到 repeat(auto-fill, …) 并拖动浏览器窗口,看列数自己变化;② 切到 1fr 2fr 1fr 再点 justify-items 的 start/center,看项目在格子内移动;③ 行模板设为 repeat(2, …) 并把项目加到 6 个,看隐式行如何接住多余的项目。

3.3

命名区域:把版式画出来

grid-template-areas 让你用 ASCII 图直接描述页面结构 —— 这大概是 CSS 里最接近「所见即所得」的一处。

讲解

用字符画出结构

每个字符串是一行,字符串里的每个名字是一个单元格,同名的相邻单元格合成一个区域,. 表示空格子。下面这张「图」就是一份可执行的版式声明:

grid-template-columns: 180px minmax(0, 1fr) 240px;
  grid-template-areas:
    "header header header"
    "rail   main   side"
    "footer footer footer";

它有两个别的手法很难替代的优点:① 可读性 —— 半年后再看这八行,你仍然能立刻说出页面长什么样;② 与 DOM 顺序解耦 —— HTML 里可以按内容优先级写(正文在前),视觉位置由这张图决定。

一条硬性规则:区域必须是矩形

CSS 规定:同名区域必须构成一个矩形。如果你把 a 画成 L 形或断开的两块,整条 grid-template-areas 声明会被整体丢弃(不是局部失效),所有项目退回到自动放置 —— 页面看起来会「莫名其妙地散架」。这是使用命名区域时唯一的重大陷阱,而好消息是它很容易用肉眼检查:同名的格子连起来必须是个方块

配合断点重排,只需重写这张图。响应式改造时不必动 HTML、也不必重写一堆属性 —— 在 @media 里把 areas 换成单列版本即可(本站案例正是这么做的,见第 4 章与第 5 章)。

什么时候值得用
页面级、有名字的区域(页头 / 导航 / 正文 / 侧栏 / 页脚)—— 区域越「大」、越稳定,命名区域越划算。
什么时候不必用
同构的卡片阵列(每张卡都一样)用 repeat(auto-fill, …) 更省事;此时给每张卡起名字反而是噪声。

关键代码

一个完整的页面骨架 css
.page {
    display: grid;
    grid-template-columns: 180px minmax(0, 1fr) 240px;
    grid-template-rows: auto minmax(0, 1fr) auto;
    /* 这三行字符串就是版式本身 */
    grid-template-areas:
      "header header header"
      "rail   main   side"
      "footer footer footer";
    gap: 24px;
    min-height: 100dvh;          /* dvh 而不是 vh:移动端地址栏收放不会导致跳动 */
  }

  .page > header { grid-area: header; }
  .page > nav    { grid-area: rail; }
  .page > main   { grid-area: main; min-width: 0; }
  .page > aside  { grid-area: side; }
  .page > footer { grid-area: footer; }

  /* 断点里只重画这张图,HTML 一行都不用改 */
  @media (max-width: 900px) {
    .page {
      grid-template-columns: minmax(0, 1fr);
      grid-template-areas:
        "header"
        "main"
        "rail"
        "side"
        "footer";
    }
  }
  
  • 字符串之间的空白无关紧要,但每个字符串的字符数必须一致(列数相同),否则声明同样会失效。
  • . 可以留出真正的空位,例如 ". main side" 让第一列在那一行完全空着。
  • 断点里的重排顺序就是移动端的阅读顺序:把 main 提到 rail 之前,读者先看到正文。

动态演示

点击格子涂色(当前画笔见下方);a / b / c 分别代表页头 / 正文 / 侧栏。

这是浏览器按你生成的 grid-template-areas 真实渲染的结果:

页头 a
正文 b
侧栏 c
还没有画出任何区域
画笔
操作
实时输出:上面每一步操作对应的 CSS css

故意画一个「断开的 a」(例如第一行涂 a a a、第三行中间再涂一个 a):状态会立刻变成「区域非法」,右边预览也会散架 —— 这就是 CSS 丢弃整条声明的结果。再清空重画成两个规整的矩形,恢复正常。

3.4

选型:三个问题定下用谁

不要在写完 Flex 之后才发现需要 Grid。选型可以压缩成三个是非问题。

讲解

一条主判据:维度

一维用 Flex,二维用 Grid。「维度」不是指「看起来有几行几列」,而是指你是否需要同时控制行与列。导航条是一排项目 —— 一维,Flex。卡片墙要让每张卡片的标题、正文、按钮在横向与纵向都对齐 —— 二维,Grid(内部配合 Flex)。

两条辅助判据

  • 尺寸由谁决定?内容驱动(项目多宽就多宽,剩余空间再分配)→ Flex。flex-basis: auto 的默认行为就是内容优先。布局驱动(版面先定,内容去适应格子)→ Grid。
  • 需要跨容器对齐吗?同一行里的多张卡片,标题必须落在同一条基线上 —— 这要求它们共享行轨道,只有 Grid 能做到。Flex 里的项目彼此独立,无法互相「对齐到同一行」。

它们最常见的合作方式:Grid 搭骨架,Flex 做零件

真实项目里两者几乎总是一起出现,分工非常清晰:页面级、区域级用 Grid(页头 / 正文 / 侧栏 / 卡片阵列),组件内部用 Flex(按钮组、标签行、卡片的图 + 文、页脚的一排链接)。本站就是这套分工:整站骨架与卡片阵列是 Grid,顶栏、控件行、卡片内部是 Flex。

反过来说也有一条经验:如果你发现自己在 Flex 容器里嵌套了三层 Flex 只为了对齐网格,那几乎一定是选错了工具;如果你为了让 Grid 里的项目「按内容宽度排列」而反复调 auto-fitjustify-content,那也可能是该用 Flex。

关键代码

同一个「卡片一行三张」,两种写法的差别 css
/* A. Flex 版:项目各自独立,宽度靠 flex-basis 约定 */
  .cards--flex {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
  }
  .cards--flex > .card {
    flex: 1 1 220px;        /* 每张卡至少 220px,剩余空间平分 */
  }
  /* ⚠️ 最后一行不足三张时,卡片会被拉宽填满整行 —— 有时这正是你要的,
     但若要保持列宽一致,就需要再加一层占位元素或改用 Grid */

  /* B. Grid 版:先定轨道,卡片只负责占位 */
  .cards--grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 16px;
  }
  /* 每个格子宽度由轨道决定,因此同一行的卡片天然等宽;
     卡片内部的标题 / 正文 / 按钮再用 Flex 排布(见 3.1) */
  .cards--grid .card {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .cards--grid .card__cta { margin-top: auto; }   /* 按钮统一吸到底部 */
  
  • 两种写法都能跑,差别在于「列宽由谁保证」:Flex 版靠每个项目自己,Grid 版靠轨道统一规定。
  • 注意 Grid 版里仍然用了 Flex —— 这不是二选一,而是分层:外层 Grid 管列,内层 Flex 管卡片内部的纵向排列与底部对齐。
  • 如果卡片内容长度差异很大(标题一两行、摘要长短不一),Grid 版加上 grid-auto-rows: 1fr 可以强制每行等高,Flex 版则做不到。

动态演示

① 你需要同时控制行和列吗?

② 项目的尺寸由谁决定?

③ 需要「跨卡片对齐」吗?(每张卡的标题、按钮落在同一条线上)

Flexbox

短标题

内容宽度决定卡片宽度,剩余空间按 flex-grow 分配。

阅读

一个明显更长的标题会占掉更多宽度

于是同一行里的按钮不在同一条线上。

阅读

中等标题

卡片宽度随内容浮动。

阅读

Grid

短标题

列宽由轨道统一规定,因此同一行的卡片永远等宽。

阅读

一个明显更长的标题会占掉更多宽度

标题换行,但按钮依然对齐在同一条线上。

阅读

中等标题

卡片等宽,节奏统一。

阅读

改一下三个问题的答案,下方结论会实时更新,并在两个面板里高亮被选中的方案。注意第二个卡片:「按钮是否落在同一条线上」正是第三条判据的直观证据。