CSS弹性盒布局

用主轴、交叉轴和弹性尺寸算法理解Flexbox,并处理内容溢出、换行、视觉顺序与生成代码中的常见错误。

难度 入门 时长 标准深度约11分钟
版本 CSS Flexible Box Layout Module Level 1
what

Flexbox是一维布局模型。容器沿主轴排列直接子元素,再沿交叉轴对齐它们。

trap

flex: 1不会自动突破内容的最小尺寸,order和反向排列也不会改变DOM与键盘顺序。

fix

先确定轴与可用空间,再明确项目的基准尺寸和伸缩规则;文本项目需要真正收缩时,检查min-inline-size: 0。

是什么,为什么存在

弹性盒布局(CSS Flexible Box Layout,通常简称Flexbox)让一个容器沿一个方向排列和分配空间。设置display: flex后,元素成为 Flex容器 ,它的直接子元素成为 Flex项目 。更深层的后代不会自动参与这个容器的Flex布局。

Flexbox解决的是内容尺寸不固定时的一维布局。导航栏中的链接长短不同,媒体对象中的正文可能溢出,按钮组也可能需要在窄容器中换行。用浮动或定位处理这些情况,需要手动补偿宽度和对齐;Flexbox把剩余空间、收缩和对齐写进同一套模型。

一维不等于永远只有一行。启用flex-wrap后,容器可以产生多条Flex行,但每一行仍独立计算弹性尺寸。需要让项目同时服从明确的行轨道和列轨道时,CSS Grid通常更合适;组件内部的一排控件、一列面板或内容驱动的换行列表则适合Flexbox。

Flexbox改变呈现,不改变文档含义。项目的DOM顺序仍决定正常阅读顺序和键盘顺序,容器与项目也保留各自的语义。布局规则应服务于正确的HTML结构,而不是用视觉重排修补错误的结构。

工作原理

Flex布局先建立一个独立的Flex格式化上下文,再收集容器的直接子元素。每个项目都有一个弹性基准尺寸、伸长因子、收缩因子和最小尺寸。浏览器按行计算这些值,然后才执行对齐。

下面的流程把最容易混淆的顺序画出来。尺寸分配发生在对齐之前,所以justify-content只处理弹性计算和自动外边距之后仍然剩下的空间。

主轴与交叉轴

主轴 由flex-direction决定。row与文字的行内轴同向,column与块轴同向;row-reverse和column-reverse交换主轴的起点与终点。主轴并不总是水平方向,书写模式和文字方向都会影响它。

交叉轴 垂直于主轴。justify-content沿主轴分配剩余空间,align-items和align-self沿交叉轴对齐项目。不要把这些属性记成水平和垂直,否则切换到flex-direction: column或竖排书写模式时很容易写反。

gap在相邻项目或相邻Flex行之间创建固定间隔,但不会在容器边缘增加一圈空间。外边缘留白仍由padding负责。主轴上的auto外边距会先吸收正的剩余空间,因此常用于把工具栏中的最后一个按钮推到末端。

基准尺寸与弹性空间

flex-basis给出弹性计算开始时的主轴尺寸。auto会参考项目的主尺寸属性和内容,长度值给出明确基准,零基准则让伸长比例较少受内容固有尺寸影响。它不是最终尺寸;最小值、最大值、内容和可用空间仍可能改变结果。

有正剩余空间时,未冻结项目按flex-grow比例获得空间。有负剩余空间时,浏览器使用flex-shrink乘以弹性基准尺寸得到缩减权重。较宽项目因此会在相同收缩因子下承担更多缩减,而不是每个项目减去完全相同的像素数。

flex简写让这三个输入保持在一起。flex: initial表示不伸长但可以收缩,flex: auto允许从自动基准伸缩,flex: none禁止弹性伸缩。单个正数形式常用于等分剩余空间,但还要同时检查自动最小尺寸和盒模型。

换行与对齐

默认的flex-wrap: nowrap把所有项目放在一条Flex行中。空间不足时,项目会尝试收缩;受到最小尺寸限制后仍放不下才会溢出。wrap根据主轴可用空间建立多条行,每条行单独执行伸长或收缩,所以不同Flex行上的项目不保证列宽对齐。

align-items控制一条行内各项目的交叉轴位置,align-content控制多条Flex行作为整体怎样占据容器的交叉轴空间。只有出现多条行并且交叉轴还有可分配空间时,align-content的差别才容易看见。想让单个项目例外对齐,应使用align-self。

默认的align-items: stretch只会拉伸交叉轴尺寸为auto的项目,并且仍受最小值与最大值约束。给项目写了明确高度后,再增加stretch通常看不到变化。这不是属性失效,而是项目已经提供了自己的交叉轴尺寸。

示例

工具栏中的自动外边距

这个工具栏只有一条Flex行。前两个链接按源顺序排列,按钮的逻辑起始外边距吸收剩余空间,把按钮推向主轴末端;gap只负责项目之间的固定间隔。

toolbar.html
<nav class="toolbar" aria-label="Project">
  <a href="#docs">Docs</a>
  <a href="#examples">Examples</a>
  <button class="search" type="button">Search</button>
</nav>

<style>
  .toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
    inline-size: 360px;
    padding: 8px;
  }

  .search {
    margin-inline-start: auto;
  }
</style>

<script>
  const toolbar = document.querySelector('.toolbar');
  const style = getComputedStyle(toolbar);
  const labels = [...toolbar.children].map((item) => item.textContent.trim());

  console.log(`display=${style.display}; direction=${style.flexDirection}; gap=${style.gap}`);
  console.log(`items=${labels.length}; first=${labels.at(0)}; last=${labels.at(-1)}`);
</script>
display=flex; direction=row; gap=12px
items=3; first=Docs; last=Search

输出确认容器采用默认的row主轴,并且三个项目的源顺序没有变化。这里使用margin-inline-start而不是margin-left,同一规则可以随书写方向切换逻辑起始边。

等宽卡片的零基准

容器宽度为420px,两个12px间隔占去24px,三个卡片平分剩下的396px。flex: 1 1 0为它们提供相同的零基准,min-inline-size: 0允许内容较长的卡片真正收缩。

equal-cards.html
<section class="cards" aria-label="Plans">
  <article class="card">Free</article>
  <article class="card">Team</article>
  <article class="card">Enterprise</article>
</section>

<style>
  .cards {
    display: flex;
    gap: 12px;
    inline-size: 420px;
  }

  .card {
    box-sizing: border-box;
    flex: 1 1 0;
    min-inline-size: 0;
    padding: 8px;
    background: #e2e8f0;
  }
</style>

<script>
  const cards = [...document.querySelectorAll('.card')];
  const widths = cards.map((card) => card.getBoundingClientRect().width);
  const style = getComputedStyle(cards[0]);

  console.log(`widths=${widths.join(',')}`);
  console.log(`grow=${style.flexGrow}; shrink=${style.flexShrink}; basis=${style.flexBasis}`);
</script>
widths=132,132,132
grow=1; shrink=1; basis=0px

测得的外框宽度都是132px。这项结论依赖示例中相同的内边距和盒模型;若卡片的边框、内边距或最小尺寸不同,只写flex: 1不能保证外框仍然等宽。

允许长文本收缩

头像保持48px,正文获得余下宽度。Flex项目默认的自动最小尺寸可能采用内容的最小内容尺寸,长且不可断开的仓库名于是会把整行撑开;把正文的逻辑最小宽度设为零后,省略号才有可用的收缩空间。

profile-row.html
<article class="profile">
  <img class="avatar" alt="" src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>" />
  <div class="details">
    <strong>Ada</strong>
    <div class="repository">frontend-platform-with-a-very-long-unbroken-name</div>
  </div>
</article>

<style>
  .profile {
    display: flex;
    gap: 12px;
    inline-size: 320px;
  }
  .avatar {
    flex: none;
    inline-size: 48px;
    block-size: 48px;
  }
  .details {
    flex: 1 1 auto;
    min-inline-size: 0;
  }
  .repository {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
</style>

<script>
  const row = document.querySelector('.profile');
  const avatar = document.querySelector('.avatar');
  const details = document.querySelector('.details');
  const repository = document.querySelector('.repository');

  console.log(`row=${row.clientWidth}; avatar=${avatar.clientWidth}; details=${details.clientWidth}`);
  console.log(`truncated=${repository.scrollWidth > repository.clientWidth}`);
</script>
row=320; avatar=48; details=260
truncated=true

行宽正好由48px头像、12px间隔和260px正文组成。truncated=true证明内容的滚动宽度大于可见宽度,而不是仅凭截图猜测省略号是否生效。

多行项目仍按源顺序排列

每个步骤的基准宽度为104px,224px容器一行能放两个项目和一个8px间隔。第五个项目进入第三行,但DOM顺序没有因换行发生改变。

wrapped-tags.html
<ul class="pipeline" aria-label="Pipeline">
  <li>Build</li>
  <li>Test</li>
  <li>Package</li>
  <li>Deploy</li>
  <li>Observe</li>
</ul>

<style>
  .pipeline {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    inline-size: 224px;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .pipeline li {
    box-sizing: border-box;
    flex: 0 0 104px;
    block-size: 28px;
    padding: 4px;
    background: #dbeafe;
  }
</style>

<script>
  const pipeline = document.querySelector('.pipeline');
  const steps = [...pipeline.children];
  const containerTop = pipeline.getBoundingClientRect().top;
  const rows = steps.map((step) => step.getBoundingClientRect().top - containerTop);

  console.log(`rows=${rows.join(',')}`);
  console.log(`source-order=${steps.map((step) => step.textContent).join('>')}`);
</script>
rows=0,0,36,36,72
source-order=Build>Test>Package>Deploy>Observe

相邻行顶端相差36px,也就是28px项目高度加8px间隔。若要让不同Flex行形成严格对齐的列,应该改用Grid轨道,而不是依赖内容碰巧得到相同宽度。

陷阱

深入 自动最小尺寸决定能否收缩

自动最小尺寸决定能否收缩

Flex项目的主轴最小尺寸默认是auto。对于非滚动项目,它可能根据内容建议尺寸、指定尺寸和替换元素的纵横比计算一个内容相关下限。这项默认值可以避免短标签和图片被压得无法识别,但也正是许多文本溢出的来源。

flex-shrink只参与分配负剩余空间,不会绕过最小尺寸。算法算出的目标尺寸低于下限时,项目会被钳制并冻结,浏览器再把仍需缩减的空间分给其他项目。若所有项目都到达下限,容器只能溢出。

min-inline-size: 0比min-width: 0更能表达意图,因为它随书写模式指向行内轴。在常见的flex-direction: row中,行内轴也是主轴;若容器改成column,需要检查的通常变成块轴最小尺寸。不要把零最小值复制到所有后代,只给确实承担可收缩内容的项目。

图片、视频和其他替换元素还带有固有尺寸与纵横比。若它们必须保持固定缩略图尺寸,flex: none配合明确的逻辑宽高通常比只写flex-shrink: 0更完整;若允许响应式缩小,则应给出最大尺寸和适合该组件的对象适配规则。

flex-basis与主尺寸

当flex-basis是auto时,浏览器会查看主尺寸属性。例如,row容器通常参考width,column容器通常参考height;主尺寸也是auto时,内容参与确定基准。把flex-basis改成非auto值后,该值成为弹性计算的起点,但最小值和最大值仍会钳制目标尺寸。

零基准适合表达“从同一起点分配空间”,自动基准适合表达“先保留内容或主尺寸,再分配余量”。两者没有普遍的优劣。按钮组可能需要自动基准以保留标签差异,价格卡片可能需要零基准以形成相同列宽。

百分比flex-basis需要一个确定的容器主轴内部尺寸作为参照。参照尺寸不确定时,百分比基准按内容处理;这就是同一个子组件放进明确宽度面板和内容驱动面板后可能得到不同结果的原因。调试时应同时检查父容器的确定尺寸来源。

width与flex-basis同时出现时,不要简单概括为一个总会覆盖另一个。flex-basis选择弹性基准,width仍可能通过auto基准、最小尺寸建议或其他盒模型约束影响结果。开发者工具中的最终尺寸和计算值比“属性优先级”口诀可靠。

每条Flex行独立分配空间

换行算法先根据项目的外部假设主尺寸把项目收集到多条行中,再对每条行分别解析弹性长度。第一行的三个项目与第二行的两个项目不会共享一个伸长因子总和。因此,flex-grow: 1的换行卡片常在最后一行变得更宽。

正剩余空间按伸长因子分配。负剩余空间则按收缩因子乘以弹性基准尺寸后的权重分配,这项缩放避免窄项目和宽项目在相同因子下都减少同样的像素。每次最小值或最大值钳制项目后,算法会冻结部分项目并继续分配,而不是一次除法就结束。

gap、外边距、边框和内边距都会减少可供内容盒使用的空间。计算等宽布局时只减去gap仍可能漏掉其他外部尺寸。最稳妥的检查方式是同时记录容器内部主尺寸、每个项目外部目标尺寸和所有间隔。

多行形成的是一组独立的一维布局,不是二维轨道系统。若设计要求同一列跨行共享宽度、项目按行列坐标定位,或最后一行必须沿用前几行的列线,应使用Grid。Flexbox更适合让每行根据自身内容自然分配。

书写模式与源顺序

row跟随当前书写模式的行内方向,不等同于固定的从左到右。direction: rtl会改变行内起点,row-reverse又会反转主轴;两者叠加时,只凭属性名猜屏幕顺序很容易出错。逻辑属性和实际浏览器检查可以减少这种假设。

Flexbox规范定义了order修改有序序组后的呈现顺序,但明确要求非视觉媒体继续使用源顺序。顺序焦点通常也遵循文档结构,而不是重新绘制后的盒子位置。把重要流程写成正确DOM顺序,能让无样式页面、辅助技术和自动化测试得到同一语义。

响应式设计中,源顺序应满足最窄和最线性的阅读方式。宽屏可以用Grid区域或不改变含义的对齐把内容摆开,而不必重排关键操作。确需视觉移动装饰性项目时,要把它与交互控件的顺序问题分开审查。

嵌套容器各自拥有布局

一个Flex容器只把直接子盒变成Flex项目。项目内部的普通后代仍按自身格式化上下文布局;若某个项目同时设置display: flex,它才会成为另一个容器,为自己的直接子元素建立一套独立的轴和尺寸计算。

因此,把align-items写在外层容器上不会对齐孙元素。先找到需要排列的元素由哪个直接父盒拥有,再把容器属性写在那个父盒上。组件多出一层包装时,这项所有权检查尤其重要。

直接位于Flex容器中的连续文本会被包进匿名Flex项目,但这个匿名盒不能像普通元素一样单独选择和设置样式。需要控制该文本的伸缩、最小尺寸或对齐时,应使用符合语义的元素把它显式包起来。

从约束开始调试

Flexbox问题通常不是缺少另一个对齐属性,而是误判了参与布局的盒子或尺寸约束。按浏览器算法的顺序检查,比反复添加简写更快定位根因。

  1. 在元素面板确认真正的Flex容器及其直接项目,排除多余包装层。
  2. 根据flex-direction、writing-mode和direction标出主轴起点与终点。
  3. 记录容器内部主尺寸,以及每个项目的flex-basis、固有内容尺寸、最小值和最大值。
  4. 判断当前行拥有正剩余空间还是负剩余空间,再检查伸长、收缩、自动外边距和gap。
  5. 最后检查justify-content、align-items、align-content和源顺序;对齐无法修复前一步的尺寸矛盾。

开发者工具的Flex覆盖层适合观察轴、行和间隔,计算样式适合确认简写展开与最小尺寸。测试数据至少应包含短内容、最长真实内容、不可断字符串、图片和本地化文本。布局只在占位文案下成立,不算完成。

延伸阅读

检查点

4个问题 · 1道输出预测题 · 1道找错题

复制为Markdown 面试题库 在GitHub上编辑 报告错误 讲清楚了吗?