1. 字体样式:别小看这些"看起来很简单"的属性
字体样式是CSS里最早接触的一批属性,也是最容易被低估的一块。很多人觉得不就是font-size和color嘛,真到做项目的时候,中英文混排、行高失调、字体回退、溢出打点……每一项都能折腾半天。我整理这部分笔记的时候,把高频踩坑点都过了一遍,发现真正值得记下来的东西其实挺多的。
1.1 font-family的字体栈:先英文后中文,别写反了
先看一个最常见的写法:
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; }这个字体栈看起来长,其实是几个关键逻辑。第一,-apple-system和BlinkMacSystemFont是macOS和iOS上直接调用系统UI字体,渲染效果最好;Segoe UI对应Windows 10/11的系统字体;后面的PingFang SC、Hiragino Sans GB是苹果系统的中文字体;Microsoft YaHei是Windows的中文字体。最后兜底sans-serif。
为什么要先列英文字体再列中文字体?因为中文字体像苹方、雅黑,它们里面其实也包含了英文字形,而且这些英文字形往往不如专门的西文字体好看。如果写的时候把中文字体放在前面,浏览器碰到一个英文字母,会先去中文字体里找字形,导致英文和数字显示得偏宽、偏呆。把英文字体放在前面,浏览器会优先用英文字体渲染英文和数字,只有遇到中文字符时才会去找中文字体,这样中英文观感都好。
项目里如果不是对品牌字体有特别要求,直接用这串系统字体栈就好,不要引入一堆web font。字重、字号的性能开销不说,中文web font动辄几MB,首页加载一个全量字体包,首屏速度直接受影响。除非海报、营销页这种强视觉场景,否则系统字体栈是最稳的选择。
1.2 font-size、line-height与"垂直居中"的真相
line-height大概是字体属性里最容易被误解的一个。先说结论:line-height不是文字高度,是行盒(line box)的高度。一个文字内容区(content area)加上上下行间距(half-leading),构成了行盒的高度。两个相邻行盒之间的基线距离,就是line-height的取值。
很多人想要一行文字在某个容器里"垂直居中",第一反应是给容器设line-height等于容器高度。这个思路对单行文本是有效的:
.button { height: 40px; line-height: 40px; text-align: center; }这样一行文字会在40px高的容器里垂直居中。原理是行盒高度等于容器高度,而且content area在行盒内是垂直居中的。但有三类情况会翻车:
- 文本跑到了第二行(或换行后),line-height等于容器高度时,两行文本会插入两倍行高,溢出容器;
- 容器高度不是固定px,而是由padding撑开,这时设line-height等于容器高度没法算;
- 图片、按钮这类inline-block或替换元素,line-height的垂直居中效果不一样。
更稳妥的做法分为两种:单行文本用display: inline-flex; align-items: center,或者直接用flex布局容器配合align-items: center。这个话题后面讲flex的时候还会展开。
line-height取值用倍数还是像素?项目里绝大多数情况用倍数,比如line-height: 1.5。倍数会基于当前font-size重新计算,修改font-size时不用跟着改line-height,这是最省心的方式。需要精确控制小标题的行高时,再用px值比如56px固定高度。
1.3 文本装饰和溢出控制:删除线、打点、段落分割线
网络热词里有一类高频搜索,比如"css 删除线"、"css 两行超出"、"css 打点"、"css实现段落分割线"。这些其实都对应文本细节属性的实战用法。
删除线很简单,就是text-decoration: line-through,通常配合<del>标签或者给价格划原价时用。需要注意text-decoration的简写顺序是text-decoration-line、text-decoration-style、text-decoration-color,所以写line-through后面可以追加wavy或颜色指定。
两行溢出打点是移动端列表、卡片标题的经典场景,完整写法如下:
.ellipsis-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }单行省略则是:
.ellipsis-1 { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }很多人搞不明白为什么两行打点不用text-overflow: ellipsis,因为text-overflow只对单行、块级容器内文本wr流的末端起作用。多行打点依赖-webkit-box伸缩盒模型的行数截断,这是-webkit内核支持的,现在主流浏览器基本都认。写的时候记得把display: -webkit-box和-webkit-line-clamp配套,缺一个效果都出不来。
段落分割线是标题与正文之间的细线。常见实现是border-bottom配合padding-bottom,还有一种更灵活的方式是用伪元素:
.divider { display: flex; align-items: center; gap: 12px; color: #333; font-size: 16px; } .divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: #e5e5e5; }这样文字两侧会各有一条弹性拉伸的横线,不管标题多长,线都会自动贴齐两侧,是一种很干净的排版效果。
1.4 字体渐变:background-clip:text的那一行魔法
"css 字体渐变"是这几年营销页、活动页的高频需求。做法不复杂:
.gradient-text { background: linear-gradient(135deg, #1a73e8, #d93025); -webkit-background-clip: text; background-clip: text; color: transparent; }核心就三步:给文字容器设一个背景(一般是渐变),把背景裁剪范围限定在文字轮廓内,再把文字本身颜色设为透明。这样文字露出来的部分就是背景的渐变效果。有两个细节容易踩坑:background-clip: text需要加-webkit-前缀,Color属性必须设置为transparent,否则背景会被文字颜色盖住,看不出效果。另外部分浏览器下给background设置完整简写时要注意顺序,先写background-image或background简写,再写background-clip,因为简写属性会把clip重置为border-box。
还有一行text-fill-color: transparent,可以在不支持background-clip的老浏览器里替代color: transparent。不过现在浏览器兼容性已经很好,这个备用写法按需加。
2. 盒子模型:整排布局崩塌的根源,就在这里
盒子模型是CSS的底层世界观,也是新手第一个"看懂了但做不对"的坎。网络上讲盒子模型的文章很多,但多数停留在"content、padding、border、margin"的罗列层面。我在复盘布局bug时发现,真正让人难受的不是不知道这四个部分,而是标准盒子与怪异盒子的混用、margin塌陷、margin合并这三件事。
2.1 content-box与border-box:一个宽度算错,一排布局就崩
标准盒子模型(content-box)下,一个元素的width只代表内容区(content)的宽度,padding和border是额外加在外面的。所以:
.box { width: 200px; padding: 20px; border: 1px solid #000; }这个元素实际占用的水平宽度是200 + 20*2 + 1*2 = 242px,而不是200px。用百分比或grid等分布局的时候,width: 25%再加padding,实际宽度变成25% + padding,一行四个盒子大概率挤爆换行。
border-box(IE的怪异盒子被CSS3采用后的正式名称)会把width定义为内容区+padding+border的总宽度,padding和border从内容区里"挤"出来。同一个例子,width: 200px时内容区实际是158px,但整体占用永远是200px。
这个问题解决的痛感极强。我自己的项目里几乎第一行就是:
*, *::before, *::after { box-sizing: border-box; }这样写以后,所有盒子的宽度直觉化了:我设置的宽度就是最终渲染宽度,padding和border不会让它“变胖”。这也是现在各类reset样式库(像normalize.css的官方推荐用法、Tailwind预置器)的默认做法。
2.2 margin塌陷:子元素的上边距,为什么跑到了父元素外面
margin塌陷这个问题,90%的新手都遇到过,但很多人根本不知道自己遇到了。最典型的情况:
<div class="parent"> <div class="child"></div> </div>.parent { background: #f0f0f0; } .child { margin-top: 30px; }你以为子元素距父容器顶边30px,实际上父容器整体被顶了下来,子元素贴在上边。这个“父容器被拖着走”的现象就是margin塌陷。
原因是父子相邻的margin重叠机制:父子元素的margin-top相邻时,会合并取两者最大值,最终作用点是父元素的外部。任何阻断二者相邻的条件都能解决这个局面,常用的有:
- 给父容器加
padding-top(而不是用子元素的margin) - 给父容器加
border-top - 给父容器加
overflow: hidden或overflow: auto - 给父容器加
display: flow-root(产生BFC但无副作用) - 父容器或子元素任意一个改为flex/grid容器(flex和grid的子项margin不会塌陷)
实际项目中我优先选display: flow-root,这是专门的BFC生成方式,不会引入滚动容器也不会产生裁剪。不过要注意度,有些团队的老代码浏览器兼容要求高,那时用padding-top替代子元素margin是最直观的。
还有一个兄弟元素的margin合并:连续两个块级元素,前者margin-bottom: 30px,后者margin-top: 20px,最终两者间距是30px而不是50px。别试图去记“谁大谁生效”,只要记住一句话:垂直方向的相邻margin会合并,取较大值。想要精确间距,兄弟之间优先用margin-bottom或margin-top单一方向控制,另一个方向设0,不要两头设。
2.3 几个容易被忽略的盒子细节:负margin、overflow与百分比
负margin现在用得少了,因为flex和grid能覆盖绝大多数场景。但有两个地方依然会用到:一个是把元素从静态流里"拉"回去,比如两列布局时给第二列设负margin实现宽度补偿;另一个是使用margin: 0 auto水平居中时,如果元素宽度大于父容器,auto负margin会让它居中后被裁剪。flex普及后,我建议能用margin: 0 auto解决的居中场景照常用,但两列布局的负margin技巧尽量少写,维护成本高。
overflow属性平时和盒子一起出现。overflow: hidden不只裁剪内容,还会创建BFC,顺带解决了margin塌陷。如果把overflow-x: hidden加到body上,会连带把overflow-y的默认值从visible改成auto,具体就是滚动条意外出现又消失的现象。
盒子模型的百分比padding还有一个妙用:padding-top: 56.25%可以实现一个按比例的占位容器,常见于视频封面占位。因为百分比padding始终基于父元素宽度计算,不随高度变化,所以padding-bottom: 100%配合宽高自适应能做正方形。这个技巧在响应式页面里很有价值。
3. flex布局:居中只是入门,真正的难点是主轴交叉轴
搜索热词里"flex布局"出现了好多次,还有一个半截代码body{background:#111;display:flex;flex-direction:column;justify-content:,看起来是用户在搜索引擎里粘贴了自己的代码片段想查答案,这种情况十有八九又是居中问题。flex的入门门槛低,两三个属性就能做出导航栏、两栏布局,但真正把flex吃透的人不算多。
3.1 flex容器上的六个必须懂的属性
flex布局的核心是容器上的两个轴:主轴和交叉轴。flex-direction决定主轴方向,row是水平、column是垂直。剩下的容器属性本质上都是在分配主轴和交叉轴上的位置或尺寸:
| 属性 | 作用轴 | 作用 |
|---|---|---|
flex-direction | 主轴 | row / column 定义排列方向 |
justify-content | 主轴 | 分配主轴剩余空间(置中、两端对齐等) |
align-items | 交叉轴 | 单个行内项目的对齐方式 |
align-content | 交叉轴 | 多行时,整行在交叉轴的对齐方式 |
flex-wrap | 主轴 | nowrap / wrap 是否换行 |
gap | 主轴+交叉轴 | 项目之间的间隔(row-gap和column-gap的简写) |
实际写代码时最常用的组合是:justify-content: center配合align-items: center,就能实现水平和垂直同时居中。这就是"css中怎么把input居中"的答案,也是最干净的居中方案:
.wrapper { display: flex; justify-content: center; align-items: center; }有一类问题在于align-content和align-items搞混。简单来说:只有flex-wrap: wrap产生了多行时,align-content才有意义,它是多行作为整体在交叉轴上的排布方式;align-items是每一行内部项目在各自行内的对齐方式。一行只有一排项目时,align-content不生效,align-items才起作用。
3.2 flex: 1到底是几个意思:flex-grow、flex-shrink、flex-basis
很多教程给你说flex: 1等于平均分配,这没错,但太粗糙了。flex: 1实际上是flex: 1 1 0%的缩写,含义拆开是:
flex-grow: 1:主轴剩余空间全部分配给这个项目flex-shrink: 1:空间不足时允许该项目缩小flex-basis: 0%:项目基础尺寸为0,空间完全依赖分配
用这三个值组合,可以实现几种经典布局:
| 简写 | 实际值 | 效果 |
|---|---|---|
flex: 1 | 1 1 0% | 平均分配剩余空间,基础尺寸为0,经典平均分栏 |
flex: auto | 1 1 auto | 以内容尺寸为基础,额外分配剩余空间 |
flex: none | 0 0 auto | 不放大不缩小,保持内容原始尺寸 |
flex: 0 1 auto | 默认值 | 不放大,可缩小 |
圣杯布局的经典flex写法是中间内容区flex: 1,左右侧栏固定宽度flex: none或flex: 0 0 200px:
.layout { display: flex; min-height: 100vh; } .sidebar { flex: 0 0 200px; } .main { flex: 1; }这个布局里,侧栏宽度200px固定,中间区域自动吸收剩余宽度。把flex-basis设为0,在容器宽度变化时中间区会先被压缩/扩张,两侧永远稳如泰山。
实际工作中临时调某个项目间距,我一般直接写flex: 1、flex: none,不显式写三个值。但头脑里要清楚每个简写的展开,特别是flex: auto在宽度自适应类场景下的表现和flex: 1完全不同。
3.3 flex布局的三个常见陷阱:min-width、溢出处理、图片变形
第一个陷阱是flex子项的min-width问题。flex容器的子项默认min-width: auto,意思是子项不能缩小到比它内容最小宽度还小。如果子项里的文本不换行(比如长链接),整个flex子项会拒绝收缩,把容器撑破。解决办法是给子项设min-width: 0,允许它被压缩到比内容更窄,配合overflow: hidden或text-overflow做省略。
.flex-child { flex: 1; min-width: 0; /* 允许子项在必要时收缩到内容宽度以下 */ }第二个陷阱是button、input等表单控件在flex容器内不收缩。原因是浏览器对表单控件有内置的默认尺寸和最小宽度,需要显式加flex: 1之外还要设width: 100%或min-width: 0。移动端导航栏按钮经常出现"flex分配了等宽,但按钮文字撑开导致溢出"的问题,就是这个原因。
第三个陷阱是flex下的图片变形。给图片容器设display: flex之后,图片作为flex子项,如果容器里设置了align-items: stretch(默认值),再给图片设width: 100%,图片高度可能被拉伸变形。解决方式是给img加align-self: flex-start,或者在img上直接object-fit: cover。
3.4 两栏、三栏与等分布局:flex怎么搭才不出错
等分布局的经典写法前面说过flex: 1,但等分布局有一个前提:项目之间要有间距,间距需要从总宽度里预留。如果每个子项flex: 1,然后直接给子项加margin: 0 10px,会发现总宽度超出。更稳的写法是容器上加gap:
.columns { display: flex; gap: 16px; } .columns > * { flex: 1; min-width: 0; }gap不会影响子项宽度计算,因为它在flex容器的空间分配中先被扣除。写margin还得考虑第一个和最后一个元素要不要补边距,写gap一劳永逸。
三栏布局无非是左右两侧固定、中间自适应,前面圣杯布局已经演示过。响应式里经常会看到flex-wrap: wrap配合flex: 0 0 calc(33.33% - 16px)做等宽卡片,这里要记得把gap占用的宽度减去,不然每一行总宽超了,卡片会掉行。
4. 定位详解:relative、absolute、fixed与sticky的适用边界
定位是CSS里"脱离常规流"的主要方式,也是理解层级、浮层、弹窗、吸顶这些交互的基础。定位本身不难,难的是把四种定位方式的参照系、脱离文档流的行为、以及跟z-index的纠缠关系搞清楚。
4.1 relative与absolute配对:参照系才是核心
position: relative自己不会脱离文档流,但它是absolute的参照系。当absolute元素找不到任何非static定位的祖先时,它会相对于初始包含块(一般是viewport)定位。这个"逃逸"行为是很多浮层位置错乱的根源。
解决的铁律是:给父容器加position: relative,子元素用position: absolute并配合top/right/bottom/left定位。
<div class="card"> <span class="badge">NEW</span> </div>.card { position: relative; width: 200px; height: 120px; } .badge { position: absolute; top: 8px; right: 8px; background: #e53935; color: #fff; padding: 2px 8px; font-size: 12px; border-radius: 4px; }absolute元素脱离文档流后,父容器会“当作它不存在”,高度塌陷。常见的错误是父容器高度只由内容撑开,但子元素absolute后不撑开父容器,导致底部的其他内容跑上来盖住浮层。解决方式有两种:一是给父容器设明确高度或min-height;二是如果浮层必须固定大小,给父容器留好占位空间。
absolute还有个非常重要的应用是垂直水平居中。以前用margin: auto需要明确宽度,用absolute则不需要:
.modal { position: absolute; inset: 0; margin: auto; width: 300px; height: 200px; }inset是top: 0; right: 0; bottom: 0; left: 0的简写,把元素四边全部钉在参照系边界上,再通过宽高和margin: auto把元素居中。这个方法对固定宽高的弹窗、提示框特别实用,不需要知道父容器尺寸。
4.2 fixed、sticky与"被transform打破"的固定定位
position: fixed最直接的语义是固定在视口里,不随页面滚动。下载按钮、回到顶部、右侧悬浮客服,都是如此。网络上"css样式固定在左右边怎么设置"这类搜索,答案就是fixed配right或left:
.float-btn { position: fixed; right: 24px; bottom: 48px; width: 48px; height: 48px; border-radius: 50%; background: #1a73e8; color: #fff; }但fixed有一个隐藏极深的坑:**如果fixed元素的任一祖先元素设置了transform、perspective、filter或will-change,它的包含块就从视口变成了那个祖先元素。**换句话说,fixed就退化成了absolute,会跟着祖先一起滚动、一起位移。
这个坑在弹窗场景经常遇到:弹窗外层容器为了做入场动画加了一个transform: scale(0.8),结果弹窗内部的下拉菜单用了fixed,打开后位置不在视口内,而变成了相对transform容器的定位,菜单位置完全错乱。
position: sticky是另一种很实用的定位,它综合了relative和fixed的特性:在父容器范围内,元素未经过阈值前是relative状态,经过阈值后固定在视口内,父容器结束则解除固定。最经典的是吸顶导航:
.nav { position: sticky; top: 0; z-index: 100; background: #fff; }sticky有几个注意点:父容器必须足够高才能看出固定效果;父容器不能有overflow: hidden,否则sticky失效;top值设多大,元素就贴在该位置。移动端滚动容器嵌套sticky有时会失效,需要检查所有祖先的overflow设置。
4.3 z-index与定位的纠缠:为什么层级会莫名错乱
z-index只作用于定位元素(relative、absolute、fixed、sticky)以及flex/grid容器内的子项。两个position默认值static的元素,z-index设多少都不生效,这是很多层级问题找不到原因的地方。
层级高低的基本判断顺序是:
- 同一父容器下,后写的DOM元素在上层;
- 非定位元素 < 定位元素(z-index为auto/0);
- 同是定位元素时,z-index数值大的在上层;
- 不同父容器的子元素比较z-index时,先看父容器的z-index,父容器层级低则子元素再高也出不来。
第四点的"父容器隔离"概念非常关键。假设A父容器z-index: 10,B父容器z-index: 20,B里一个子元素的z-index只有1,但A里另一个子元素z-index高达999。最终看到的层级是B父子元素整体在A父子元素之上,A那个z-index: 999的元素不会跑到B上面。因为父容器把子元素的层级“打包”了。
很多诡异的弹窗被遮挡问题,排查顺序基本是这个:检查弹窗元素是否有定位;检查弹窗和遮挡元素是否在不同父容器;检查父容器之间是否有z-index差异;检查是否有元素创建了新的层叠上下文(transform、filter、opacity小于1都会创建)。顺序对了,问题排查很快。
4.4 定位与布局的整体思考:能用flex解决的尽量别用绝对定位
最后说一个定位的实际选型原则。很多新手喜欢用absolute把所有元素钉在容器内的某个位置,比如标题、按钮分别 absolute 定位。这在固定尺寸的静态页里没问题,但页面一改版、一响应式,所有坐标都得重排,维护成本很高。
我现在的习惯是:能用flex布局的自然流排布,就不用绝对定位;绝对定位主要留给浮层、角标、弹窗这类“脱离文档流”确实需要的东西;sticky用于吸顶、吸底这类交互;fixed用于固定操作按钮和全局浮层。定位是用来解决特殊情况的,不是用来替代常规布局的,这样想,代码的可维护性能高不少。
定位的最后一个实操技巧:如果需要在运行时动态获取某个元素在视口内的精确位置,getBoundingClientRect()返回的top、left、width、height可以直接用于fixed定位的坐标计算,比自己推算样式直观得多。这在实现自定义tooltip、右键菜单时非常好用。
整理这套笔记的过程中,我发现CSS学习最大的障碍不是记不住属性,而是搞不清楚每个属性在文档流中的角色和相互作用。字体样式是文字的呈现基础,盒子模型是布局的地基,flex是灵活排布的主力,定位是脱离常规流的例外机制。把这四层关系理顺,再去看网络上各种奇奇怪怪的布局技巧,基本都能看懂背后的逻辑。实际项目里多写几次、多调几次样式,这些知识才会真正变成自己的手感。