CSS布局(五):Flex——让布局更灵活

第五章:Flex------让布局更灵活

上一章我们认识了 BFC,明白了"容器隔离内外"的核心思想。这一章,我们来学习一个更强大的容器------Flex 容器 。它不仅能隔离内外(本身就是 BFC),还提供了控制内部项目排列的超级能力,让你轻松实现水平垂直居中、等分布局、自适应排版等传统布局难以做到的效果。


5.1 传统布局的痛点

在没有 Flex 和 Grid 的"旧时代",CSS 布局就像是在用原始工具搭建积木。很多今天一行代码就能解决的问题,当时需要各种 hack 和计算。我们拿两个最常见的需求为例:垂直居中等宽分布,看看当年开发者是怎么"绕路"的。


一、垂直居中:一个需求,一堆"野路子"
1. 绝对定位 + 负边距(最经典,但最死板)
css 复制代码
.parent {
  position: relative;
}
.child {
  width: 200px;
  height: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -50px;   /* 高度的一半 */
  margin-left: -100px;  /* 宽度的一半 */
}

原理 :先让子元素的左上角定位到父容器的中心点(top:50%; left:50%),再用负边距把子元素向左、向上拉回自身宽高的一半。

痛点

  • 必须知道子元素的精确宽高,否则无法计算负边距。
  • 如果子元素宽高变化(如响应式),就得重新计算,不够灵活。
  • 代码看起来像在"打补丁",逻辑不直观。

后来有了 transform: translate(-50%, -50%) 可以替代负边距,但仍需要 top/left 定位,且早期浏览器兼容性不佳。

2. line-height + text-align(只对单行文本友好)
css 复制代码
.parent {
  height: 200px;
  line-height: 200px;    /* 让行高等于容器高度 */
  text-align: center;
}
.child {
  display: inline-block; /* 转成行内块,让 text-align 生效 */
  line-height: 1.2;      /* 重置子元素行高,避免文字错位 */
  vertical-align: middle;
}

原理 :每个字都有字体大小(font-size) ,但浏览器为一行文字分配的垂直空间是行高(line-height) 。行高可以大于字体大小,多出来的部分会平均分配到文字的上方和下方,称为"半间距"。行高 = 字号 + 行间距。行框的高度就是行高,文字默认在行框内垂直居中(因为上下空白相等)。所以,如果让行高等于容器的高度,且只有一行文字,那这行文字自然就在容器正中间

这个方法的本质是利用行框的单行特性实现居中。当容器高度等于行高时,只有一个行框,且文字在该行框内垂直居中,所以文字就在容器正中间。

一旦内容变成多行,问题就来了:

  • 每一行都会生成一个与行高等高的独立行框。
  • 第一行行框从容器顶部开始,文字在其行框内居中 → 第一行文字在容器中居中。
  • 第二行行框紧贴第一行行框底部,完全超出容器底部 → 第二行文字显示在容器外面。
  • 后续行依此类推,全部溢出容器。

最终结果:只有第一行文字在容器中居中,其他行都跑到外面去了。整个内容块并没有真正实现垂直居中。所以,这个方法只适用于单行文本。多行文本或包含多个元素的复杂内容,用这个方法就会暴露各种问题。

相比之下,Flex 和 Grid 用更简洁的语法就能解决任意行数的居中问题,而且没有这些副作用。

3. table-cell 法(需要额外表格结构)
css 复制代码
.parent {
  display: table;
}
.child {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}

原理 :利用表格单元格的 vertical-align 属性天然支持垂直居中。

痛点

  • 需要为父元素设置 display: table,子元素设置 display: table-cell,改变了元素的原本语义。
  • 表格布局对响应式支持不好,宽度分配有时会出乎意料。
  • 如果子元素不止一个,还要考虑表格行的结构,代码臃肿。

二、等宽分布:浮动布局的"连环坑"

目标:一个导航栏,三个按钮等宽水平排列,填满父容器。

html 复制代码
<nav class="nav">
  <a href="#">首页</a>
  <a href="#">产品</a>
  <a href="#">关于</a>
</nav>
1. 浮动 + 宽度百分比(最常用,但问题最多)
css 复制代码
.nav {
  overflow: hidden; /* 尝试清除浮动?等一下,这只是触发BFC,不是清除 */
}
.nav a {
  float: left;
  width: 33.33%;    /* 等宽 */
  box-sizing: border-box;
  padding: 10px;
  text-align: center;
}

问题一:高度塌陷

所有子元素都浮动了,父元素 .nav 失去高度,背景和边框都缩成一条线。需要额外清除浮动:

css 复制代码
.nav::after {
  content: "";
  display: table;
  clear: both;
}

或者加一个空的 <div style="clear: both;"> ------ 为了布局多一个无意义的标签。

问题二:宽度计算不精确
33.33% 是循环小数,不同浏览器对像素舍入处理不同,可能导致三个按钮宽度总和大于或小于父容器,引发换行。开发者经常要用 calc() 微调:

css 复制代码
width: calc(33.3333% - 0.1px); /* 这种"黑魔法"很常见 */

问题三:垂直对齐

按钮里的文字如果高度不一,浮动本身不处理垂直对齐,还得靠 line-heightpadding 硬调。

2. inline-block 方案(也有自己的麻烦)
css 复制代码
.nav {
  font-size: 0; /* 干掉换行符产生的空格 */
}
.nav a {
  display: inline-block;
  width: 33.33%;
  box-sizing: border-box;
  font-size: 16px; /* 恢复字体大小 */
}

问题

  • 需要额外处理 inline-block 之间的空格(父元素 font-size:0,子元素再恢复)。
  • 垂直对齐仍需 vertical-align 介入。
  • 宽度计算同样面临小数像素问题。

三、Flex 登场:用 Flex 实现垂直居中和等宽分布

回头看看这些解法,你会发现它们都在绕路 :为了达到一个简单的效果,不得不处理一堆副作用(塌陷、空格、计算......)。Flex 的设计目标很明确------直接解决排列问题,不让开发者操心副作用

用 Flex 实现垂直居中:

css 复制代码
.parent {
  display: flex;
  align-items: center;    /* 垂直居中 */
  justify-content: center; /* 水平居中 */
}
/* 子元素宽高任意,无需知道 */

用 Flex 实现等宽导航:

css 复制代码
.nav {
  display: flex;
}
.nav a {
  flex: 1;                /* 等分剩余空间 */
  text-align: center;
  padding: 10px;
}
/* 没有浮动塌陷,没有空格问题,没有宽度计算 */

Flex 的核心思想:把布局逻辑交给容器,让容器告诉项目怎么排。开发者只需要声明"我要居中""我要等分",剩下的浏览器自动计算。

传统布局不是不能实现效果,而是为了达到效果,你需要额外处理很多与目标无关的细节。Flex 的出现,把这些细节都封装进了浏览器底层。它让你从"怎么让浮动不塌陷"的泥潭里解脱出来,真正专注于"我想怎么排列"本身。

下一节,我们就来系统学习 Flex 的核心概念,看看它是如何做到这一点的。

5.2 Flex 容器:一个更智能的 BFC

当你给一个元素设置 display: flexdisplay: inline-flex,它就变成了一个 Flex 容器。这个容器天然具备 BFC 的特性------内部怎么折腾都不会影响外部。但更重要的是,它内部有一套全新的排列规则。

1. 为什么叫 flex?

flex 这个名称来自英文单词 flexible,意为"弹性的、可伸缩的"。

在传统的布局模型中,元素的尺寸和位置往往是固定的,或者只遵循简单的文档流规则。当容器大小发生变化时(例如浏览器窗口缩放),元素不会自动调整来填满剩余空间或避免溢出。

Flex 布局的核心能力正是为了解决这个问题:容器可以根据可用空间的大小,自动伸缩子元素的尺寸与排列方式

  • 如果空间有多余,子元素可以被拉长(flex-grow)来填满空间
  • 如果空间不足,子元素可以被压缩(flex-shrink)来适应空间
  • 如果一行放不下,子元素可以自动换行(flex-wrap

因此,flex 这个名称准确描述了该布局模型的本质:一个具有弹性能力的盒子模型

理解要点:flex ≠ 固定布局,flex = 动态分配空间。

2. 主轴与交叉轴

Flex 容器内部有两条核心轴线:主轴交叉轴

  • 主轴(main axis):项目排列的方向(默认水平从左到右)
  • 交叉轴(cross axis):垂直于主轴的另一个方向

你可以通过几个简单的属性,控制项目在主轴和交叉轴上的对齐方式、排列顺序、换行行为等。

一、主轴(main axis)

主轴是 flex 布局中的方向轴 。它决定了子元素在容器中排列的主方向------也就是元素们会沿着哪条线一个接一个地排下去。

主轴的朝向由 flex-direction 属性控制:

属性值 主轴方向 视觉表现
row 水平方向 从左向右排列
row-reverse 水平反向 从右向左排列
column 垂直方向 从上向下排列
column-reverse 垂直反向 从下向上排列

为什么叫"主"轴?

因为这个轴是 flex 布局中最主要的参考线 。所有与位置分配、间距控制相关的核心操作,都沿着这个轴进行。例如:

  • justify-content:在主轴上对齐元素、分配剩余空间
  • flex-grow / flex-shrink:在主轴上决定元素的伸缩行为

可以这样理解:主轴 = 主方向 = 元素排队的方向。就像一列队伍,所有人面朝同一个方向排列,这个方向就是"主"方向。

理解要点:先确定主轴方向,才能理解其他所有 flex 属性的行为。

二、交叉轴(cross axis)

交叉轴是 flex 布局中的第二根轴 ,它的定义非常简单:永远垂直于主轴

  • 当主轴是水平方向(rowrow-reverse)时,交叉轴就是垂直方向(上→下)
  • 当主轴是垂直方向(columncolumn-reverse)时,交叉轴就是水平方向(左→右)

为什么叫"交叉"轴?

"交叉"(cross)一词来源于几何中的十字交叉 ------两根轴相互垂直,在中心点相交,形成一个十字形。主轴是横线,交叉轴就是竖线;主轴是竖线,交叉轴就是横线。两者始终成 90° 直角

交叉轴的作用与主轴互补:

  • 主轴负责"排队方向"
  • 交叉轴负责"对齐方向"

具体来说,与交叉轴相关的核心属性包括:

  • align-items:单行内,每个元素在交叉轴上的对齐方式
  • align-self:单个元素在交叉轴上的独立对齐方式
  • align-content:多行时,整组行在交叉轴上的空间分配

理解要点:交叉轴 = 与主轴垂直相交的那条轴。它不负责排列顺序,只负责在垂直方向上"摆正"元素的位置。

三、主轴与交叉轴的关系总结
对比维度 主轴 交叉轴
方向 flex-direction 决定 永远垂直于主轴
作用 排列顺序 + 空间分配 单行或多行的整体对齐
核心属性 justify-contentflex-growflex-shrink align-itemsalign-selfalign-content
记忆关键词 主方向、排队 十字交叉、垂直对齐

一个简单的判断方法

  1. 先看 flex-direction 决定主轴是水平还是垂直
  2. 主轴方向确定了,交叉轴就是剩下的那个方向
  3. 沿着主轴的方向用 justify 系列属性控制
  4. 沿着交叉轴的方向用 align 系列属性控制

5.3 Flex 核心属性(一):容器上的属性

这些属性写在父容器上,决定整个内部项目的布局方式。

1. flex-direction:决定主轴方向
css 复制代码
.container {
  display: flex;
  flex-direction: row;      /* 默认值:水平从左到右 */
}

可选值:rowrow-reversecolumncolumn-reverse

2. justify-content:主轴上的对齐方式
含义
flex-start(默认) 向主轴起点对齐
flex-end 向主轴终点对齐
center 居中对齐
space-between 两端对齐,项目之间间隔相等
space-around 每个项目两侧间隔相等
space-evenly 项目之间及两端间隔都相等
css 复制代码
.container {
  justify-content: center;  /* 水平居中 */
}
3. align-items:交叉轴上的对齐方式(单行)
含义
stretch(默认) 如果项目未设置高度,将占满整个容器高度
flex-start 向交叉轴起点对齐
flex-end 向交叉轴终点对齐
center 居中对齐
baseline 以第一行文字的基线对齐
css 复制代码
.container {
  align-items: center;      /* 垂直居中(单行) */
}
4. flex-wrap:是否换行
含义
nowrap(默认) 不换行,可能压缩项目宽度
wrap 换行,项目按自身尺寸排列
wrap-reverse 换行,但行序反转
5. align-content:交叉轴上的行对齐方式(多行)

当有多行时,用 align-content 控制行与行之间的间距。取值与 justify-content 类似:flex-startflex-endcenterspace-betweenspace-aroundstretch



5.4 Flex 核心属性(二):项目上的属性

这些属性写在子项目上,控制单个项目的表现。

1. flex-grow:放大比例
  • 默认 0,即不放大
  • 如果有剩余空间,按比例分配。例如所有项目 flex-grow: 1,则均分剩余空间
2. flex-shrink:缩小比例
  • 默认 1,空间不足时会缩小
  • 设为 0 则不会缩小
3. flex-basis:项目在主轴上的初始尺寸
  • 默认 auto,即项目本来的大小
  • 可以设为固定值,如 100px

常用缩写:flex: 1 代表 flex: 1 1 0%,即可以放大缩小,初始尺寸为 0。

⚠️ 容易混淆的点

剩余空间 ≠ 总空间

  • 总空间 = 父容器宽度
  • 剩余空间 = 总空间 − 所有子项 flex-basis 之和

所以:

  • flex-basis: auto:不重新分配"已经占用的那部分",只分"多出来的"
  • flex-basis: 0%:连"已经占用的"都按比例重新分
举个例子(父容器 900px,两个子项)
情况 A:flex-basis: auto
  • 子项1:内容宽 200px,flex-grow: 1
  • 子项2:内容宽 100px,flex-grow: 2
  • 剩余空间 = 900 − (200 + 100) = 600
  • 子项1 增加:200 + 600 × 1/3 = 400px
  • 子项2 增加:100 + 600 × 2/3 = 500px
情况 B:flex-basis: 0%
  • 基准都是 0
  • 总空间 900 全部按 1:2 分配
  • 子项1 = 300px
  • 子项2 = 600px
4. align-self:覆盖容器的 align-items
  • 允许单个项目有不一样的对齐方式
  • 取值同 align-items
5. order:排列顺序
  • 默认 0,数值越小越靠前

5.5 实战案例:从零搭建常见布局

案例一:导航栏(水平居中 + 等分布局)
html 复制代码
<nav class="navbar">
  <a href="#">首页</a>
  <a href="#">产品</a>
  <a href="#">服务</a>
  <a href="#">关于</a>
</nav>
css 复制代码
.navbar {
  display: flex;
  justify-content: space-around;   /* 均匀间隔 */
  background: #333;
  padding: 1rem;
}
.navbar a {
  color: white;
  text-decoration: none;
}
案例二:垂直居中(经典弹窗)
html 复制代码
<div class="modal">
  <div class="content">我是内容</div>
</div>
css 复制代码
.modal {
  display: flex;
  justify-content: center;
  align-items: center;      /* 同时水平和垂直居中 */
  height: 300px;
  border: 1px solid #ccc;
}
案例三:圣杯布局(页眉 + 中间三列 + 页脚)
html 复制代码
<div class="layout">
  <header>页眉</header>
  <div class="main">
    <aside class="left">左侧边栏</aside>
    <article class="center">主要内容</article>
    <aside class="right">右侧边栏</aside>
  </div>
  <footer>页脚</footer>
</div>
css 复制代码
.layout {
  display: flex;
  flex-direction: column;
  min-height: 100vh;        /* 让布局撑满全屏 */
}
.main {
  display: flex;
  flex: 1;                  /* 中间区域占满剩余高度 */
}
.left, .right {
  flex: 0 0 200px;          /* 固定宽度 200px */
  background: #f0f0f0;
}
.center {
  flex: 1;                  /* 自适应占满剩余宽度 */
  background: #fff;
}
案例四:响应式卡片列表(自动换行)
html 复制代码
<div class="card-list">
  <div class="card">卡片1</div>
  <div class="card">卡片2</div>
  <div class="card">卡片3</div>
  <div class="card">卡片4</div>
</div>
css 复制代码
.card-list {
  display: flex;
  flex-wrap: wrap;          /* 允许换行 */
  gap: 1rem;                /* 项目之间的间距 */
}
.card {
  flex: 1 1 300px;          /* 基础宽度200px,可放大缩小 */
  min-width: 150px;         /* 防止过小 */
  background: lightblue;
  padding: 1rem;
  text-align: center;
}
一、排队入场规则

想象这样一个场景:

  • 有一条 人行道(容器),宽度不固定(可宽可窄)
  • 每个 人(卡片) 都有一个 理想占位(flex-basis: 300px)
  • 排队规则如下:
规则 1:先按理想占位试

每个人先说:"我要 300px 宽"

规则 2:如果人行道不够 → 换行

比如人行道只剩 100px,下一个人要 300px 站不下 → 直接换到下一行

规则 3:换行后,这一行重新分配

这一行里的所有人,把这一整行的宽度 重新按比例分 (因为有 flex-grow: 1


二、代码
css 复制代码
.card {
  flex: 1 1 300px;
  /* 相当于 flex-grow: 1; flex-shrink: 1; flex-basis: 300px; */
}
场景 1:容器宽度 = 900px
  • 第 1 人:要 300px ✅ 站下(剩 600)
  • 第 2 人:要 300px ✅ 站下(剩 300)
  • 第 3 人:要 300px ✅ 站下(剩 0)
  • 第 4 人:要 300px ❌ 不够 → 换行

第一行:3 个人

剩余空间 = 0(刚好 3×300),不需要分配

第二行:1 个人

一个人占满整行(因为有 flex-grow:1

→ 宽度 = 900px(虽然它只想 300)

👉 一行 3 个,一行 1 个


场景 2:容器宽度 = 700px
  • 第 1 人:要 300 ✅(剩 400)
  • 第 2 人:要 300 ✅(剩 100)
  • 第 3 人:要 300 ❌ 剩 100 不够 → 换行

第一行:2 个人

剩余空间 = 700 − 600 = 100

flex-grow:1 平分 → 每人多得 50

最终:350px + 350px

第二行:2 个人

(第 3、第 4 人)同样逻辑 → 每人 350px

👉 一行 2 个,一行 2 个


场景 3:容器宽度 = 500px
  • 第 1 人:要 300 ✅(剩 200)
  • 第 2 人:要 300 ❌ 剩 200 不够 → 换行

第一行:1 个人

一个人占满 500px

第二行、第三行... 同理

👉 每行 1 个


三、关键结论(用排队逻辑总结)

flex-basis 决定"试站位时我要多大"
试完发现不够 → 换行
换行后,这一行内部再用 flex-grow 重新分

所以:

  • 容器变窄 → 每一行能站的人变少 → 一行个数变少
  • 容器变宽 → 每一行能站的人变多 → 一行个数变多

这就是 "变大变小,一行个数会变" 的根本原因。


  • flex-basis: 0% → 所有人从 0 开始抢

  • flex-basis: auto → 先保留自己的 size

  • flex-basis: 300px → 理想占位 300px(类似"先要 300,多了再分,少了换行")

排队逻辑完全一样,只是"理想占位"从 0 / auto 变成了一个固定值。


5.6 与 BFC 的呼应

学完 Flex 你会发现,我们一直在使用 BFC 的"隔离"思想:

  • Flex 容器本身就是 BFC,所以它不会被外部浮动干扰,也不会让内部边距跑出去。
  • 内部的 Flex 项目虽然可以灵活排列,但它们之间的 margin 仍然会合并(因为它们在同一个 BFC 内),但 Flex 容器和外部的 margin 不会合并。

这正是我们第四章所说的:容器隔离内外,内部随便折腾,但内部仍然遵循自己的规则。


本章小结

  • Flex 容器 = BFC + 强大的内部排列能力
  • 核心在两根轴:主轴(justify-content)和交叉轴(align-items
  • 项目可以通过 flex 属性灵活分配空间
  • 解决了传统布局中垂直居中、等分布局、自适应等难题

下一章,我们将走进 Grid 布局,体验二维网格的强大,你会发现它同样是 BFC 的延伸------一个更精细的"隔离容器"。

相关推荐
ZC跨境爬虫2 小时前
3D 地球卫星轨道可视化平台开发 Day6(SEC数据接口扩展实现)
前端·microsoft·3d·html·json·交互
qq_12084093712 小时前
Three.js 工程向:EffectComposer 后处理链路与色彩管理
开发语言·前端·javascript
|晴 天|2 小时前
评论系统与情感分析
前端·ai·typescript
沉默中爆发的IT男2 小时前
BGP基础配置实验总结
linux·服务器·前端
朝阳392 小时前
前端学习方法(含前端成神之路)
前端·学习方法
张元清2 小时前
head.tsx 就是一个 React 组件:用 loader 数据动态生成 SEO meta
前端·javascript·面试
lemon_yyds3 小时前
Element UI 实践踩坑- date-picker 组件 定制化type="daterange"
前端·css
Alice-YUE3 小时前
ai对话平台中的functioncalling+mcp
前端·笔记·学习·语言模型
MXN_小南学前端3 小时前
Vue 视频上传实战:视频预览、MediaRecorder 压缩与自定义上传
前端·vue.js