Skip to content

CSS

说明

共 15 题,难度 ⭐ ~ ⭐⭐⭐,覆盖布局、定位、动画性能、响应式、CSS 新特性等前端高频 CSS 面试知识点。

1. 水平垂直居中的 N 种方式 ⭐

列举至少 5 种水平垂直居中的方案,并说明各自适用场景。

考察点:Flex / Grid / 定位 / transform / 表格布局

方案汇总

css
/* ① Flex(最常用) */
.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* ② Grid */
.parent {
  display: grid;
  place-items: center;
}

/* ③ 绝对定位 + transform */
.parent {
  position: relative;
}
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

/* ④ 绝对定位 + margin auto(需已知宽高) */
.parent {
  position: relative;
}
.child {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 200px;
  height: 100px;
}

/* ⑤ 绝对定位 + 负 margin(需已知宽高) */
.parent {
  position: relative;
}
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 200px;
  height: 100px;
  margin-top: -50px;
  margin-left: -100px;
}

/* ⑥ 行内元素居中 */
.parent {
  text-align: center;
  line-height: 300px; /* 等于容器高度 */
}
.child {
  display: inline-block;
  vertical-align: middle;
  line-height: normal;
}

/* ⑦ table-cell */
.parent {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}
.child {
  display: inline-block;
}

对比

方案是否需要已知宽高兼容性适用场景
FlexIE11+首选,几乎万能
Grid place-items现代浏览器代码最少,二维布局
absolute + transformIE9+不影响父级布局
absolute + margin autoIE8+弹窗、模态框
absolute + 负 marginIE6+兼容古老浏览器
line-height❌(但父级需固定高度)全兼容单行文本
table-cellIE8+多行文本垂直居中

追问延伸

  • margin: auto 在 Flex 容器中的特殊行为?(自动吸收剩余空间)
  • 如何让一个不定宽高的弹窗在视口居中?(Flex / Grid / fixed + transform)
  • place-items: center 是哪两个属性的简写?

2. 什么是 BFC?如何触发?解决了什么问题? ⭐⭐

解释 BFC(Block Formatting Context)的概念、触发方式和实际应用。

考察点:BFC 规则、外边距折叠、浮动清除

BFC 是什么

BFC(块级格式化上下文)是页面中的一块独立渲染区域,内部元素的布局不会影响外部,外部也不会影响内部。

触发 BFC 的方式

css
/* 任意一种即可触发 BFC */
overflow: hidden | auto | scroll;   /* 不为 visible */
display: flow-root;                  /* 最语义化,推荐 */
display: flex | inline-flex;
display: grid | inline-grid;
display: table-cell | table-caption;
float: left | right;                /* 不为 none */
position: absolute | fixed;         /* 脱离文档流 */
contain: layout | content | paint;
column-count / column-width;        /* 多列布局 */

BFC 能解决的三大问题

① 外边距折叠(Margin Collapsing)

html
<!-- 问题:两个相邻块的 margin 会合并 -->
<div style="margin-bottom: 20px">A</div>
<div style="margin-top: 30px">B</div>
<!-- 实际间距是 30px 不是 50px -->

<!-- 解法:让其中一个在独立的 BFC 中 -->
<div style="margin-bottom: 20px">A</div>
<div style="display: flow-root">
  <div style="margin-top: 30px">B</div>
</div>
<!-- 间距 = 20 + 30 = 50px -->

② 浮动元素导致父容器高度塌陷

css
/* 问题:子元素全部浮动,父容器高度为 0 */
.parent {
  /* 解法:触发 BFC */
  display: flow-root; /* 推荐 */
  /* 或 overflow: hidden; */
}
.child {
  float: left;
}

③ 浮动元素与文本的环绕问题

css
/* 让侧边栏和主内容互不干扰 */
.sidebar {
  float: left;
  width: 200px;
}
.main {
  overflow: hidden; /* 触发 BFC,不被 sidebar 覆盖 */
}

display: flow-root vs overflow: hidden

特性flow-rootoverflow: hidden
语义✅ 专门用于创建 BFC❌ 本意是裁剪溢出内容
副作用截断 box-shadowdropdown 等溢出内容
兼容性Chrome 58+IE8+

追问延伸

  • IFC(Inline Formatting Context)和 BFC 的区别?
  • 为什么 display: flex 的子元素不会发生外边距折叠?
  • overflow: hidden 创建 BFC 是"特性"还是"规范行为"?

3. position 的 5 个值及其区别?sticky 的触发条件? ⭐

说明 CSS 定位的 5 个值,重点阐述 sticky 的工作原理。

考察点:定位模型、包含块、sticky 边界

5 种定位值

是否脱离文档流参照物特点
static默认值,top/left 等无效
relative自身原始位置偏移后原始空间仍保留
absolute最近的非 static 祖先完全脱离,不占空间
fixed视口(viewport)滚动不动,但 transform 祖先会改变参照物
sticky❌(切换前)最近可滚动祖先在 relative 和 fixed 间切换

sticky 深入

css
.sticky-header {
  position: sticky;
  top: 0;
}

sticky 的生效条件(全部满足才有效):

  1. 必须指定 top / bottom / left / right 中至少一个
  2. 父容器的高度必须大于 sticky 元素本身(否则没有滚动空间)
  3. 父容器不能有 overflow: hidden | auto | scroll(否则不是视口触发滚动)
  4. sticky 元素在其包含块内活动,滚出包含块后"失效"
sticky 工作过程:

 ┌─── 包含块(父容器)───┐
 │                       │
 │  ┌─ sticky 元素 ─┐    │  ← 正常流中跟随滚动(相当于 relative)
 │  └───────────────┘    │
 │                       │  ← 滚到 top: 0 时"粘住"(相当于 fixed)
 │                       │
 └───────────────────────┘  ← 滚出包含块时,sticky 元素被"带走"

fixed 的"陷阱"

css
/* 当祖先有 transform 时,fixed 不再相对于视口! */
.ancestor {
  transform: translateZ(0); /* 或任何非 none 的 transform */
}
.child {
  position: fixed; /* 此时相对于 .ancestor 定位,而非视口 */
}

追问延伸

  • position: sticky 能否同时指定 topbottom?(可以,用于"弹性粘性")
  • 为什么 transform 会改变 fixed 的参照物?(创建了新的包含块)
  • 如何用 JS 判断 sticky 是否已经"粘住"了?(IntersectionObserver)

4. Flex 中 flex: 1 到底等于什么?flex-basiswidth 的优先级? ⭐⭐

深入解析 Flex 三个子属性的计算规则。

考察点:Flex 简写展开、flex-grow/shrink/basis 计算

flex 简写展开

简写flex-growflex-shrinkflex-basis
flex: 1110%
flex: auto11auto
flex: none00auto
flex: 0 auto01auto(= initial
flex: 2 1 100px21100px

⚠️ flex: 1flex-grow: 1flex: 1 会将 flex-basis 设为 0%,而单独写 flex-grow: 1flex-basis 保持默认的 auto

flex-basis vs width 优先级

优先级链:
flex-basis (非 auto) > width > 内容宽度

            flex-basis: auto 时退回到 width
css
.item {
  width: 200px;
  flex-basis: 100px; /* 实际使用 100px 作为初始主轴尺寸 */
}

.item2 {
  width: 200px;
  flex-basis: auto; /* 退回到 width: 200px */
}

.item3 {
  /* 无 width,无 flex-basis */
  /* 使用内容宽度 (min-content) */
}

flex-grow 分配计算

容器宽度: 600px
子元素 A: flex: 1, 内容 100px  → basis = 0%
子元素 B: flex: 2, 内容 200px  → basis = 0%
子元素 C: flex: 0 0 100px      → basis = 100px

剩余空间 = 600 - 0 - 0 - 100 = 500px
A 分得: 500 × (1/3) ≈ 166.7px
B 分得: 500 × (2/3) ≈ 333.3px
C 分得: 0(grow = 0)

最终: A=166.7px, B=333.3px, C=100px

flex-shrink 压缩计算

容器宽度: 300px
子元素 A: flex: 0 1 200px
子元素 B: flex: 0 2 200px

溢出 = 200 + 200 - 300 = 100px
加权总和 = 1×200 + 2×200 = 600
A 压缩: 100 × (1×200/600) = 33.3px → 最终 166.7px
B 压缩: 100 × (2×200/600) = 66.7px → 最终 133.3px

注意: shrink 是按 shrink × basis 的加权比例压缩!

追问延伸

  • min-width: 0 在 Flex 布局中有什么用?(解决文本溢出不压缩的问题)
  • flex-basis: 0%flex-basis: 0px 有区别吗?
  • 为什么 flex: 1 的元素设了 width 却不生效?

5. 说说层叠上下文和 z-index 的规则 ⭐⭐

解释层叠上下文的创建条件和 z-index 的比较规则。

考察点:层叠顺序、层叠上下文创建、z-index 失效

层叠顺序(从低到高)

最底层
  ↓ 层叠上下文背景/边框
  ↓ z-index < 0 的定位元素
  ↓ 块级元素(普通流)
  ↓ 浮动元素
  ↓ 行内/行内块元素
  ↓ z-index: 0 / auto 的定位元素
  ↓ z-index > 0 的定位元素
最顶层

创建层叠上下文的条件

css
/* 以下任意一种都会创建新的层叠上下文 */
position: relative/absolute/fixed + z-index 不为 auto
opacity < 1
transform 不为 none
filter 不为 none
will-change: transform / opacity / filter
isolation: isolate
mix-blend-mode 不为 normal
-webkit-overflow-scrolling: touch
contain: layout / paint

z-index 的"失效"场景

html
<!-- 经典问题:为什么 z-index: 9999 还是被遮挡? -->
<div class="parent-a" style="position: relative; z-index: 1;">
  <div class="child" style="position: absolute; z-index: 9999;">
    我 z-index 很大,但...
  </div>
</div>
<div class="parent-b" style="position: relative; z-index: 2;">
  我遮住了 child!
</div>

<!--
  原因: child 的 z-index 只在 parent-a 的层叠上下文内比较
  parent-a (z:1) vs parent-b (z:2) → parent-b 在上
  child 的 9999 和 parent-b 不在同一个上下文中,无法比较
-->
层叠上下文树:

root (z: auto)
├── parent-a (z: 1)  ← 创建了层叠上下文
│   └── child (z: 9999) ← 只在 parent-a 内部排序
└── parent-b (z: 2)  ← 和 parent-a 同级比较
                        parent-b 赢(2 > 1)

最佳实践

css
/* 用 isolation 创建干净的层叠上下文,不需要 z-index */
.modal-container {
  isolation: isolate; /* 创建新的层叠上下文,内部 z-index 不外泄 */
}

.modal-backdrop { z-index: 1; }
.modal-content  { z-index: 2; }
/* 这些 z-index 只在 .modal-container 内部生效 */

追问延伸

  • opacity: 0.99 会创建新的层叠上下文,有什么实际影响?
  • isolation: isolate 的使用场景?
  • 为什么 position: fixed 元素有时会被 transform 容器的子元素遮挡?

6. CSS 选择器优先级如何计算? ⭐

说明 CSS 特异性(Specificity)的计算规则和 !important 的最佳实践。

考察点:特异性计算、层叠规则

优先级计算

特异性表示为 (a, b, c) 的权重向量:

a = ID 选择器的数量
b = 类选择器 / 属性选择器 / 伪类的数量
c = 元素选择器 / 伪元素的数量

比较规则: 从左到右逐级比较,大的赢
选择器(a, b, c)说明
*(0, 0, 0)通配符不计数
div(0, 0, 1)1 个元素
.cls(0, 1, 0)1 个类
#id(1, 0, 0)1 个 ID
div.cls(0, 1, 1)1 类 + 1 元素
#id .cls div(1, 1, 1)1 ID + 1 类 + 1 元素
div div div div div(0, 0, 5)5 个元素,但仍 < 1 个类
:is(#id)(1, 0, 0):is() 取参数中最高特异性
:where(#id)(0, 0, 0):where() 特异性始终为 0
style=""内联样式高于任何选择器
!important高于内联,进入 important 层级比较

层叠优先级全貌

1. 用户代理 !important
2. 用户 !important
3. 作者 !important          ← 你写的 !important
4. @layer 中的 !important   (注意: layer 中 important 优先级反转)
5. 作者普通样式
6. @layer 中的普通样式
7. 用户普通样式
8. 用户代理普通样式

!important 最佳实践

css
/* ❌ 滥用 !important */
.header { color: red !important; }
.header.active { color: blue !important; } /* 不得不继续加 !important */

/* ✅ 合理使用场景 */
/* 1. 覆盖第三方库样式 */
.ant-btn-primary {
  background-color: #6366f1 !important;
}

/* 2. 工具类(Tailwind 式) */
.hidden { display: none !important; }
.sr-only { position: absolute !important; /* ... */ }

/* ✅ 更好的替代方案 */
/* 用 :where() 降低第三方选择器的特异性 */
:where(.third-party) .my-component {
  color: blue; /* 不需要 !important 也能覆盖 */
}

追问延伸

  • :is():where() 的特异性区别?实际中怎么利用?
  • CSS @layer 如何管理第三方样式的优先级?
  • 11 个类选择器能否胜过 1 个 ID 选择器?(不能,不同维度不进位)

7. transform 动画为什么比 left/top 性能好? ⭐⭐⭐

解释浏览器渲染流水线和合成层(Compositing Layer)。

考察点:渲染流水线、GPU 加速、合成层

浏览器渲染流水线

JS / CSS → Style → Layout → Paint → Composite
                     ↑         ↑        ↑
                   回流      重绘    合成
                 (Reflow)  (Repaint) (Composite)
属性变化触发阶段性能开销
width / height / left / topLayout → Paint → Composite🔴 高(回流)
color / background / box-shadowPaint → Composite🟡 中(重绘)
transform / opacity仅 Composite🟢 低(合成)

为什么 transform 快?

left/top 动画:
每一帧 → 重新计算布局 → 重新绘制整个图层 → 合成
         ↑ 主线程阻塞                    ↑ GPU

transform 动画:
第一帧 → 提升到独立合成层(GPU 纹理)
后续帧 → GPU 直接变换纹理坐标            ← 主线程空闲!

合成层(Compositing Layer)

css
/* 以下属性会让元素提升到独立的合成层 */
transform: translateZ(0);         /* hack 触发 */
will-change: transform;           /* 规范方式 */
opacity 动画中;
position: fixed;
/* 等等 */

合成层的"层爆炸"问题

css
/* ❌ 不要给大量元素加 will-change */
.list-item {
  will-change: transform; /* 1000 个列表项 → 1000 个 GPU 纹理 → 显存爆炸 */
}

/* ✅ 按需触发 */
.card {
  transition: transform 0.3s;
}
.card:hover {
  will-change: transform; /* hover 时才提升 */
}
/* 或用 JS 在动画开始前加,动画结束后移除 */

动画性能对比

css
/* ❌ 性能差 */
.box {
  position: absolute;
  transition: left 0.3s, top 0.3s;
}
.box:hover {
  left: 100px;
  top: 50px;
}

/* ✅ 性能好 */
.box {
  transition: transform 0.3s;
}
.box:hover {
  transform: translate(100px, 50px);
}

追问延伸

  • 如何用 Chrome DevTools 查看合成层?(Layers 面板)
  • will-change 什么时候该用,什么时候不该用?
  • CSS contain: strict 和合成层有什么关系?

8. rem/em/vw/vh 的区别?移动端适配方案对比 ⭐⭐

对比 CSS 长度单位,并梳理主流移动端适配方案。

考察点:响应式单位、移动端适配

单位对比

单位参照物特点
px设备独立像素(CSS 像素)绝对值,不响应字体/视口变化
em父元素font-size嵌套时会复合放大(坑)
rem根元素 <html>font-size全局统一基准
vw视口宽度的 1%100vw = 视口宽度
vh视口高度的 1%移动端有地址栏问题
dvh动态视口高度排除地址栏,推荐
svh / lvh最小/最大视口高度针对移动端浏览器
cqw / cqh容器查询单位相对于最近 container 祖先

em 的"坑"

css
.parent { font-size: 16px; }
.child  { font-size: 1.5em; }  /* 16 × 1.5 = 24px */
.grandchild { font-size: 1.5em; }  /* 24 × 1.5 = 36px!不是 24px */
/* em 层层复合,难以预测 */

移动端适配方案

① rem + 动态 html font-size(淘宝 flexible 方案,已过时)

javascript
document.documentElement.style.fontSize =
  document.documentElement.clientWidth / 10 + 'px'
// 375px 设备 → html font-size = 37.5px
// 设计稿 100px → 100 / 37.5 ≈ 2.667rem

② viewport + vw(当前主流)

css
/* 设计稿 750px,1px = 100/750 vw */
.box {
  width: 13.333vw;  /* = 100px / 750 × 100 */
  font-size: 4.267vw; /* = 32px / 750 × 100 */
}

/* 配合 postcss-px-to-viewport 自动转换 */
/* postcss.config.js */
/* { viewportWidth: 375 } → 直接写 px,编译时自动转 vw */

③ 响应式 + CSS 变量 + clamp()(现代方案)

css
:root {
  --font-base: clamp(14px, 2vw + 8px, 18px);
  --spacing: clamp(8px, 1.5vw, 16px);
}

body {
  font-size: var(--font-base);
}
方案优点缺点
rem + JS兼容性好需要 JS、闪屏
vw纯 CSS、线性缩放大屏不封顶、文字可能过大
clamp()有上下限控制计算稍复杂

追问延伸

  • 移动端 vh 为什么不准?dvh 怎么解决的?
  • postcss-px-to-viewportpostcss-pxtorem 各自原理?
  • 如何处理 1px 问题?(下一题详解)

9. 如何实现 1px 细线?为什么会有 1px 问题? ⭐⭐

解释设备像素比导致的 1px 问题,并给出解决方案。

考察点:DPR(设备像素比)、物理像素、CSS 像素

为什么有 1px 问题

CSS 1px ≠ 物理 1px

在 DPR = 2 的设备上(如 iPhone):
CSS 1px → 实际渲染 2 个物理像素 → 线条看起来"粗"

设计稿期望的 1px 是物理像素的 1px
但 CSS 的 1px 是设备独立像素

解决方案

① transform scale(最常用)

css
.border-1px {
  position: relative;
}
.border-1px::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: #000;
  transform: scaleY(0.5); /* DPR=2 时缩放到 0.5 */
  transform-origin: 0 0;
}

/* 四边边框 */
.border-1px-all::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid #000;
  transform: scale(0.5);
  transform-origin: 0 0;
  width: 200%;
  height: 200%;
  pointer-events: none;
}

② viewport meta + rem(整体缩放)

javascript
const dpr = window.devicePixelRatio
const scale = 1 / dpr
document.querySelector('meta[name="viewport"]').setAttribute(
  'content',
  `width=device-width,initial-scale=${scale},user-scalable=no`
)
// DPR=2 → initial-scale=0.5 → CSS 1px = 物理 1px
// 但所有尺寸都要乘以 dpr 来补偿

border-image / SVG

css
.border-1px {
  border-bottom: 1px solid transparent;
  border-image: url("data:image/svg+xml,...") 2 stretch;
}

④ 0.5px(现代方案)

css
/* iOS 8+ 和现代 Chrome 支持 */
.border-hairline {
  border: 0.5px solid #000;
}

/* 渐进增强 */
@media (-webkit-min-device-pixel-ratio: 2) {
  .border-1px {
    border-width: 0.5px;
  }
}
方案兼容性复杂度推荐度
transform scale全兼容⭐⭐⭐
viewport 缩放全兼容高(影响全局)⭐⭐
0.5pxiOS 8+, Chrome 56+⭐⭐⭐(现代项目)
border-imageIE11+

追问延伸

  • window.devicePixelRatio 返回什么?DPR=3 的设备有哪些?
  • Retina 屏下图片模糊怎么处理?(2x/3x 图、srcset
  • image-rendering: pixelated 什么时候用?

10. CSS 变量 vs Sass 变量?运行时 vs 编译时? ⭐⭐

对比 CSS 自定义属性和预处理器变量的本质区别。

考察点:CSS 自定义属性、级联继承、运行时动态

核心区别

维度CSS 变量 (--var)Sass 变量 ($var)
生效时机运行时编译时
级联/继承✅ 参与 CSS 级联❌ 编译后消失
JS 交互✅ 可读写
作用域选择器级别(DOM 树)块级作用域({} 内)
默认值var(--color, red)$color: red !default
计算能力有限(calc() 内)强大(循环、条件、函数)
媒体查询内修改

CSS 变量的级联能力

css
:root {
  --primary: #6366f1;
  --spacing: 16px;
}

.dark-theme {
  --primary: #818cf8;  /* 暗色主题自动覆盖 */
}

.card {
  background: var(--primary);
  padding: var(--spacing);
}

/* 媒体查询中修改 */
@media (max-width: 768px) {
  :root {
    --spacing: 8px;  /* 移动端间距缩小,所有用到 --spacing 的地方自动更新 */
  }
}

JS 操作 CSS 变量

javascript
const root = document.documentElement

root.style.setProperty('--primary', '#06b6d4')

const value = getComputedStyle(root).getPropertyValue('--primary')

root.style.setProperty('--mouse-x', `${e.clientX}px`)
root.style.setProperty('--mouse-y', `${e.clientY}px`)

主题切换实践

css
/* 定义主题变量 */
:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --primary: #6366f1;
}

:root[data-theme="dark"] {
  --bg: #0a0a0a;
  --text: #ededed;
  --primary: #818cf8;
}

/* 使用变量 */
body {
  background: var(--bg);
  color: var(--text);
}
javascript
function toggleTheme() {
  const isDark = document.documentElement.dataset.theme === 'dark'
  document.documentElement.dataset.theme = isDark ? 'light' : 'dark'
}

CSS 变量 + Sass 最佳组合

scss
// _variables.scss — 编译时计算
$breakpoints: (sm: 640px, md: 768px, lg: 1024px);
$colors: (primary: #6366f1, secondary: #06b6d4);

// 编译时生成 CSS 变量
:root {
  @each $name, $color in $colors {
    --color-#{$name}: #{$color};
  }
}

// 运行时使用 CSS 变量
.button {
  background: var(--color-primary);
}

追问延伸

  • CSS 变量能否用于媒体查询的条件?(不能,@media (min-width: var(--bp)) 无效)
  • @property 注册自定义属性有什么用?(类型、继承、初始值,可做渐变动画)
  • PostCSS 的 postcss-custom-properties 插件做了什么?

11. @layer 是什么?解决了什么问题? ⭐⭐

解释 CSS 级联层(Cascade Layers)的工作原理和实际应用。

考察点:CSS 级联层、样式优先级管理

为什么需要 @layer

传统 CSS 优先级管理的痛点:

1. 引入第三方库(如 Ant Design)→ 选择器特异性高
2. 你的样式想覆盖它 → 不得不写更高特异性或 !important
3. 引入另一个库 → !important 大战开始 💥

@layer 的解决方案: 把样式分层,层之间的优先级 > 选择器特异性

基本用法

css
/* ① 声明层顺序(越后面优先级越高) */
@layer reset, base, components, utilities;

/* ② 把样式放入对应层 */
@layer reset {
  * { margin: 0; padding: 0; box-sizing: border-box; }
}

@layer base {
  body { font-family: system-ui; line-height: 1.5; }
  a { color: var(--primary); }
}

@layer components {
  .btn { padding: 8px 16px; border-radius: 4px; }
  .card { border: 1px solid #e5e7eb; }
}

@layer utilities {
  .hidden { display: none; }
  .text-center { text-align: center; }
}

层优先级规则

① 未分层的样式 > 任何层内的样式
② 层按声明顺序排列,后声明的优先级高
③ 层内部按正常的特异性规则比较

@layer reset, base, components, utilities;

优先级排序(从低到高):
reset < base < components < utilities < 未分层样式

实战:管理第三方库

css
/* 把第三方样式放入低优先级层 */
@layer third-party, custom;

@import url('antd.css') layer(third-party);

@layer custom {
  /* 你的样式,不需要高特异性或 !important 就能覆盖 antd */
  .ant-btn {
    border-radius: 8px;
  }
}

@layer!important 的反转

css
@layer A, B;
/* 正常样式: B > A(后声明的赢)*/
/* !important: A > B(先声明的赢!完全反转) */

@layer A {
  .box { color: red !important; }   /* ← 这个赢 */
}
@layer B {
  .box { color: blue !important; }
}
/* .box 的 color 是 red */

追问延伸

  • @layer@scope 的区别?各自解决什么问题?
  • Tailwind CSS v3.4+ 是如何利用 @layer 的?
  • 匿名层(@layer { ... } 不写名字)的优先级如何?

12. contain 属性是什么?如何用于性能优化? ⭐⭐⭐

解释 CSS Containment 的工作原理和性能优化策略。

考察点:CSS 容器限制、渲染优化、内容可见性

contain 是什么

contain 告诉浏览器:这个元素的内部变化不会影响外部,从而让浏览器跳过不必要的计算。

各值含义

限制效果
layout内部布局不影响外部内部元素改变不触发外部回流
paint内部绘制不溢出类似 overflow: hidden,不渲染可视区域外的内容
size元素的大小不依赖子元素必须显式设置宽高,子元素变化不影响父元素尺寸
style计数器、quotes 等不外泄较少使用
strict= size layout paint最强限制
content= layout paint + style推荐:适用于大多数场景

实战:长列表优化

css
/* 每个列表项独立包含,互不影响 */
.list-item {
  contain: content;
  /* 浏览器知道: 改变某个 item 的内容不需要重算整个列表 */
}

content-visibility(更强力的优化)

css
.card {
  content-visibility: auto;
  contain-intrinsic-size: auto 300px;
}
content-visibility: auto 的效果:

┌────────────── 视口 ──────────────┐
│  ┌─ card 1 ─┐  正常渲染          │
│  └──────────┘                    │
│  ┌─ card 2 ─┐  正常渲染          │
│  └──────────┘                    │
└──────────────────────────────────┘
   ┌─ card 3 ─┐  跳过渲染!         ← 屏幕外
   └──────────┘  但保留占位高度
   ┌─ card 4 ─┐  跳过渲染!
   └──────────┘

contain-intrinsic-size: auto 300px
→ 告诉浏览器"在我没渲染时,假设我高 300px"
→ 防止滚动条跳动
→ auto 关键字:渲染过一次后记住真实高度

性能收益

某电商列表页实测(500+ 商品卡片):

不加 contain:
  首次渲染: ~800ms
  滚动帧率: ~45fps

加 content-visibility: auto:
  首次渲染: ~200ms(⬇ 75%)
  滚动帧率: ~60fps

追问延伸

  • content-visibility: auto 和虚拟列表(Virtual Scroll)的区别?各自适用场景?
  • contain-intrinsic-size: auto 300px 中的 auto 有什么用?
  • contain: strict 什么时候会出问题?(忘记设宽高 → 元素塌陷为 0)

13. CSS Grid 和 Flex 各自适用什么场景? ⭐

对比 Grid 和 Flex 的设计理念和最佳使用场景。

考察点:布局模型选择

核心区别

维度FlexboxGrid
维度一维(主轴方向)二维(行 + 列)
布局方向内容驱动(Content-first)容器驱动(Layout-first)
对齐沿主轴 / 交叉轴行列交叉的单元格
适合组件内部排列页面整体布局

Flex 最佳场景

css
/* ① 导航栏 / 工具栏 */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* ② 水平/垂直居中 */
.center {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* ③ 等分容器 */
.tabs {
  display: flex;
}
.tab {
  flex: 1;
}

/* ④ 自适应布局(一个固定 + 一个撑满) */
.sidebar-layout {
  display: flex;
}
.sidebar { width: 240px; flex-shrink: 0; }
.main { flex: 1; }

Grid 最佳场景

css
/* ① 经典页面布局 */
.page {
  display: grid;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
}
.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

/* ② 卡片网格(自动填充) */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
}

/* ③ 不规则布局(跨行跨列) */
.dashboard {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}
.widget-large {
  grid-column: span 2;
  grid-row: span 2;
}

选择决策树

需要排列一组元素?
├── 只需要一个方向(水平或垂直)?
│   └── ✅ Flexbox
├── 需要行和列同时控制?
│   └── ✅ Grid
├── 元素数量不确定,需要自动换行?
│   ├── 每行/列的元素数量重要? → Grid (auto-fill)
│   └── 只需要自然流动? → Flex (flex-wrap)
└── 经典圣杯/双飞翼布局?
    └── ✅ Grid (grid-template-areas)

可以组合使用

css
/* Grid 做页面级布局,Flex 做组件级布局 */
.page {
  display: grid;
  grid-template-columns: 240px 1fr;
}

.toolbar {
  display: flex;
  gap: 8px;
  align-items: center;
}

追问延伸

  • repeat(auto-fill, ...)repeat(auto-fit, ...) 的区别?
  • Grid 的 subgrid 是什么?解决了什么问题?
  • 为什么说 Flex 是"内容驱动",Grid 是"布局驱动"?

14. :has() 选择器能做什么? ⭐⭐

给出 :has() 选择器的 3 个以上实际用例。

考察点:CSS 新特性、"父选择器"

:has() 是什么

:has() 是 CSS 中期盼已久的关系选择器(常被称为"父选择器"),它可以根据后代/兄弟的状态来选择祖先或兄弟元素。

css
/* 选中"包含 img 的" .card */
.card:has(img) {
  grid-column: span 2;
}

/* 选中"后面紧跟 p 的" h2 */
h2:has(+ p) {
  margin-bottom: 8px;
}

实际用例

① 表单验证状态

css
/* 包含无效 input 的 label 标红 */
.form-group:has(input:invalid) {
  border-color: red;
}
.form-group:has(input:invalid) label {
  color: red;
}

/* 包含 focus 的 input 的表单组高亮 */
.form-group:has(input:focus) {
  box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
}

② 条件布局

css
/* 有图片的卡片用水平布局,没图片的用垂直布局 */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

.card:not(:has(img)) {
  display: flex;
  flex-direction: column;
}

③ 全局主题响应

css
/* 当页面中有打开的 modal 时,禁止 body 滚动 */
body:has(.modal.open) {
  overflow: hidden;
}

/* 当 sidebar 展开时,main 区域缩窄 */
.layout:has(.sidebar.expanded) .main {
  margin-left: 240px;
}

④ 空状态处理

css
/* 列表为空时显示提示 */
.list:not(:has(.list-item)) {
  display: grid;
  place-items: center;
  min-height: 200px;
}
.list:not(:has(.list-item))::after {
  content: "暂无数据";
  color: #9ca3af;
}

⑤ 星级评分(纯 CSS)

css
/* 鼠标悬停在某颗星上时,它和前面的星都高亮 */
.rating:has(input:nth-of-type(3):hover) label:nth-of-type(-n+3) {
  color: gold;
}

兼容性与注意事项

浏览器支持版本
Chrome105+
Safari15.4+
Firefox121+
Edge105+
css
/* 渐进增强写法 */
@supports selector(:has(*)) {
  .card:has(img) { /* 现代浏览器 */ }
}

追问延伸

  • :has() 的性能怎样?浏览器是如何优化的?
  • :has() 能否替代一些 JavaScript 的 DOM 判断逻辑?
  • :has(:is(.a, .b)):has(.a, .b) 有区别吗?

15. 如何实现宽高比固定的容器? ⭐⭐

对比 aspect-ratio 属性和传统 padding-top hack。

考察点:padding-top hack、aspect-ratio、响应式容器

现代方案:aspect-ratio

css
/* 16:9 视频容器 */
.video-container {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
}

/* 正方形头像 */
.avatar {
  width: 80px;
  aspect-ratio: 1;
  border-radius: 50%;
  object-fit: cover;
}

/* 配合 min/max 限制 */
.card-image {
  width: 100%;
  aspect-ratio: 4 / 3;
  max-height: 400px; /* aspect-ratio 会在 max-height 处被截断 */
  object-fit: cover;
}

传统方案:padding-top hack

css
/*
  原理: padding 的百分比是相对于 **父元素宽度** 计算的
  所以 padding-top: 56.25% = 9/16 × 100%
*/
.video-wrapper {
  position: relative;
  width: 100%;
  padding-top: 56.25%; /* 16:9 */
  height: 0;
}
.video-wrapper > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

对比

维度aspect-ratiopadding-top hack
代码量1 行5+ 行
直观性16 / 9 一目了然56.25% 需要计算
子元素正常排列必须绝对定位
响应内容内容可以撑开容器内容不影响容器高度
兼容性Chrome 88+, Safari 15+IE8+

aspect-ratio 的交互行为

css
.box {
  width: 300px;
  aspect-ratio: 2 / 1;
  /* 默认 height = 150px */
}

/* 当显式设置 height 时,aspect-ratio 失效 */
.box {
  width: 300px;
  height: 200px;
  aspect-ratio: 2 / 1; /* 被忽略,height 优先 */
}

/* 但 min/max-height 可以和 aspect-ratio 配合 */
.box {
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: 500px; /* 宽度很大时,高度被限制在 500px */
}

常见宽高比速查

比例aspect-ratiopadding-top应用场景
16:916 / 956.25%视频、Banner
4:34 / 375%传统图片
1:11100%头像、缩略图
3:23 / 266.67%相册
21:921 / 942.86%超宽屏

追问延伸

  • aspect-ratioobject-fit 的区别?分别作用于什么?
  • 在 Flex / Grid 子项上使用 aspect-ratio 需要注意什么?(可能被 stretch 覆盖)
  • 如何用 @container 查询实现响应式宽高比切换?

用心学习,用代码说话 💻