做前端这几年,我越来越觉得CSS不是“能不写就不写”的边角料,而是决定页面质感的主战场。尤其当你在一个看起来功能完整的页面上反复调字号、行距、对齐时,才会明白字体和文本属性这几十个不显眼的属性,几乎就是“高级感”的来源。这是我在前端开发系列里的第三篇,上一篇讲完了CSS选择器和样式引入方式,这一篇我们集中把font和text这套“常用元素属性”掰开揉碎:字体怎么选、字号怎么定、行高怎么设、对齐怎么排、省略号怎么做、渐变色怎么画,全部围绕“从字体到文本”这一条线展开。不管你是刚学会写选择器的新手,还是做完两三个项目但总觉得页面不够精致的前端新人,这篇都能帮你真正把文字这层皮做到位。篇幅会偏长,建议先收藏再往下读。
1. 字体属性:把“默认字体”变成“设计字体”的第一道关卡
1.1 font-family:这行代码决定了页面性格
font-family是浏览器渲染文字时首先会查的属性。很多新人写字体时喜欢只给一个名字就收工,比如font-family: "微软雅黑";,结果在Mac上打开,系统找不到微软雅黑,直接落回默认中文字体,有时候是苹方,有时候是宋体。页面一旦变成宋体,气质立刻倒退十年。这不是浏览器傻,而是你没有把候选字体列全。
一个合格的前端,写字体栈要像写路由表一样,依次给出候选方案。我常用的全局字体栈是这样:
html { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; }这里面的逻辑并不复杂:-apple-system和BlinkMacSystemFont让macOS和iOS优先用San Francisco系列,"Segoe UI"是Windows 10以上系统的默认无衬线,Roboto 主要覆盖Android设备,"PingFang SC"是苹果生态的中文中文字体,"Microsoft YaHei"就是微软雅黑。这样不同系统打开页面,都能优先看到自家平台上最舒服的字体。最后加一个sans-serif兜底,意思是前面全都找不到时,让浏览器选一个默认无衬线来渲染。
这里有几个细节要记住。第一,中文字体名如果包含空格或者由多个单词组成,一定要加引号,比如"PingFang SC"、"Microsoft YaHei"、"Hiragino Sans GB"。第二,font-family的查找是从左到右,找到第一个可用的字体就停,不会帮你“智能地挑选好看的那个”,所以顺序就是优先级。第三,字体名不能写错,尤其是拼音型字体,拼错一个字母,整个栈都白搭,浏览器会直接跳过它。
实操心得:我会把全局字体定义放在html而不是body上,这样所有后续元素都天然继承这个字体族,统一性更好。如果某个组件要换字体,再单独覆盖font-family。另外,遇到“为什么我的页面字体和设计稿不一样”的问题,先别急着改代码,打开DevTools的Computed面板,拉到Font列表,看看当前元素实际渲染的是哪个字体,再回头检查字体栈的顺序,往往一下就能找到原因。
1.2 font-size:px、rem、em、%之间的真实关系
很多人从入门起就习惯用px设置字号,比如font-size: 14px。px在屏幕上的确稳定,一像素就是一像素,设计稿也特别好对。但它的短板也很明显:这是绝对单位,用户如果在浏览器设置里调大默认字号,你的页面不会跟着变。在移动端做适配时,px也无法随着视口宽度自动变化。所以现在项目里更适合的做法是把px、rem、em配合使用。
rem是相对于html根元素的字号。假设浏览器默认根字号是16px,那么1rem = 16px,0.875rem = 14px。em则是相对于当前元素自身的字号,但因为字号会从父级继承,所以嵌套层级越多,em的缩放就越容易累积出“复利效应”。举个例子:
html { font-size: 16px; } .parent { font-size: 0.875em; } /* 浏览器按父级16px计算,得到14px,但会覆盖父级字号? */ .child { font-size: 0.875em; } /* 实际是 12.25px,而不是14px,因为基准变成14px了 */这不是一个容易一眼看穿的结果,所以我的建议是:全局字号和组件的基础尺寸用rem,局部需要跟着当前字号一起缩放时用em,比如按钮的padding、图标大小、badge文字。不要在大范围嵌套里用em控制正文字号,否则到后面调一处全乱,除非你真的在做那种“整体等比缩放”的特殊场景。
现在做响应式字体,我更推荐clamp():
font-size: clamp(14px, 2vw + 10px, 20px);这行的意思是:基础值2vw + 10px在视口宽度变化时跟着变,但结果小于14px就取14px,大于20px就取20px。这种写法在窄屏手机上不会小到看不清,在宽屏显示器上也不会大到撑破布局。
注意:如果你打算用rem做无障碍优化,最好不要把html的font-size写死成固定px。用户在浏览器的“字体大小”设置里调整的是默认字号,浏览器会把根字号一起放大。你写死之后,这个机制就失效了。所以更稳妥的做法是基础要就留在默认16px,或者用百分比设置根字号,比如font-size: 100%,然后正文全部用rem。
1.3 字重和字型:不是设置了100到900就都有对应字体
font-weight支持从100到900的数值,也支持normal和bold。难点在于:字体文件里不一定真的包含9套字重。对中文网页来说,大多数系统中的字体只有regular和bold两档,你脑袋里想着“细一点”,写在CSS里是font-weight: 300,浏览器找不到300的字重,可能会直接用400渲染,或者用算法强行把400压细,效果往往会发虚。尤其在小字号屏幕上,这种“伪细体”的观感很差。
所以如果你需要真正细字重的标题,比如苹方Light、思源黑体Light,最好通过@font-face加载对应字重的字体文件,并明确font-weight范围。或者使用可变字体(variable font),一个文件包含整个连续字重轴,不过在中文网页里可变字体的文件体积和兼容性还都是现实约束,不是所有项目都能上。
font-style就简单一些,只有normal、italic、oblique。中文没有真正的italic字体,浏览器强行把正体倾斜一下,反而会破坏笔画结构。所以我一般建议中文正文别开斜体,如果是引用英文单词或书名,用italic是符合语义的。
font-variant最常用的是small-caps,能把英文小写字母渲染成小号大写。这个属性对中文没用,适合英文品牌、缩写这类场景。如果你在做英文内容展示,可以用它制造一点“仪式感”,但在中文页面里基本可以忽略。
1.4 行高:单行垂直居中的秘密,也是一个继承陷阱
行高在font相关属性里属于特殊存在,因为它直接影响文本的垂直排布。最推荐的写法是不带单位:
body { line-height: 1.6; }不带单位的值,意思是“当前元素font-size的1.6倍”。它的最大优势是继承时不会把事情搞复杂。子元素会拿到“1.6”这个数字,然后基于自己的font-size重新计算行高。如果你写成line-height: 160%,继承的却是父级已经算好的“具体像素值”,再传给子元素。一旦子元素字号变大,行高却还是基于父级算出的值,就很容易出现上下文字挤在一起或者间距不平衡。
单行垂直居中是很多初学者的第一个坑。容器高度固定成40px,里面只有一行文字,直接把line-height也设成40px,视觉上就居中了。这个方法简单有效,适合按钮、标签、提示条。但要注意,英文里如果包含y、g、p这种下部突出的字母,视觉重心会略偏下,精确一点可以用flex布局配合align-items: center。我现在写按钮仍然习惯用line-height,因为代码更短,但如果按钮内部有图标或者文本需要换行,还是得老老实实上flex。
多行文本的行高计算同样容易翻车。比如一个固定高度80px的卡片,字号14px,行高1.6,单行行高22.4px,理论上能塞三行多一点。如果忘记算卡片padding,第三行可能就溢出或者顶到边缘,视觉上很挤。计算时永远把padding包含进去,养成习惯。
1.5 font 综合速写:一行代码的取舍
如果页面某处需要同时设置斜体、字重、字号、行高、字体族,可以合并成一行:
font: italic 700 16px/1.5 "PingFang SC", "Microsoft YaHei", sans-serif;顺序规则很死板:font-style、font-variant、font-weight这三个可选的放在最前面,顺序可交换;中间必须写font-size,如果需要行高,就在字号后面加/行高;最后必须写font-family。字号和字体族不可省略,其他都可以省。
这个简写最大的坑是“重置”效应。font会把你没有提到的属性全部重置为默认值,比如font-stretch、font-size-adjust。如果你在前面定义过这些冷门属性,然后再写一次font缩写,它们全部会静默归零。所以在项目里我的习惯是:只有在一个全新文本组件需要完整定义字体属性时,才使用font简写;日常只是把字号从14px改成16px,就直接写font-size: 16px,不要动缩写。
2. 文本属性:让文字在画面里“各就各位”
2.1 text-align、text-indent、text-transform:基础但决定精致度
text-align看起来简单,但.justify这个值最容易踩坑。它会让一行文字两端对齐,等宽拉开间距。中文排版里,如果一行只有几个字,中间的空白会被拉伸得特别大,像是字和字之间突然插入了空格,阅读体验非常难受。移动端屏幕窄,一行的字数本来就少,我基本不会在正文里用justify。如果你确实需要两端对齐,可以配合text-align-last: left,让最后一行不强制撑开。
text-indent是首行缩进,通常用来做段落开头空两格。单位选em最合适,比如text-indent: 2em;这样首行缩进正好是两个字宽,和字号大小无关。用px的话,一旦字号变化缩进就失配。这个属性还可以玩一些花活,比如把负值缩进和overflow: hidden组合起来隐藏列表默认符号,但我更建议不要这样“借”文本属性,容易出兼容问题。
text-transform负责大小写转换,包括uppercase、lowercase、capitalize。它只改变视觉呈现,不影响DOM里的实际内容,所以对屏幕阅读器和SEO来说,文本原文是什么还是什么。中文场景下主要用在英文文案、品牌名、价格标签上,把产品型号统一转大写,这样前端代码里写iPhone还是IPHONE,视觉上都是统一的大写,后端要改文案也方便。
2.2 letter-spacing、word-spacing、white-space:控制文字间的呼吸感
先说letter-spacing,也就是字间距。很多好看的标题,其实不是字体本身多特别,而是你把字间距拉开到0.05em或者0.1em,整个字就“贵”了起来。注意这里我推荐用em,而不是px,因为字间距应该跟随字号变化。中文字体如果字间距太大,会断断续续像单独的字块,所以一般正文不要设置超过0.02em,标题可以适当放宽。
word-spacing是词间距,它只作用于英文、数字空格等可以分词的场景,对中文几乎无效。不要指望用它在中文里增加字和字的空隙,那个是letter-spacing的活。
white-space决定源码里的空格和换行怎么处理。默认normal,会把多个空格压缩成一个,也会让文本在容器边界自动换行。nowrap表示一行装到底,不换行,这个常和省略号一起用。pre保留源码中的空格和换行,类似<pre>标签,适合展示代码、日志文本。这里有一个常见误用:想保留换行又不想让超长文本溢出,更多人需要的是pre-wrap,它保留空格和换行,但允许在必要时换行。pre-line则只保留换行,压缩空格。这几个值的区别可以按需求对号入座。
2.3 文本装饰与阴影:下划线也能玩出细节
text-decoration是传统的下划线、删除线、上划线。完整写法现在拆成了text-decoration-line、text-decoration-color、text-decoration-style、text-decoration-thickness。比如做链接下划线时,想要一条偏下的细线:
a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }text-underline-offset可以控制下划线和文字的距离,这个参数在很多UI设计里是点睛之笔,直接决定链接看起来是“贴上去”还是“浮在上面”。另外,如果想去掉下划线,别只用display: inline这类手段,直接用text-decoration: none最干净。
text-shadow是给文字加阴影,写法是四个参数依次为x偏移、y偏移、模糊半径、颜色。多组阴影用逗号分隔,可以模拟描边、发光、甚至立体的效果。示例:
.title { text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 8px rgba(255, 255, 255, 0.5); }不过阴影也不能乱加,尤其正文里加一个若有若无的投影,会让文字在浅色背景上发灰。文本描边则更偏向特效,-webkit-text-stroke能做到,比如做一个“空心的文字标题”,但它是非标准属性,而且会覆盖字体的笔画细节,适合当氛围元素,不适合当正文。
2.4 文本超出一行:省略号的标准姿势
这是开发中使用频率最高的三个属性组合,我直接给模板:
.ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }三个属性缺一不可:white-space: nowrap让文字不换行,overflow: hidden让超出的部分不可见,text-overflow: ellipsis在裁切处显示省略号。这个写法只适用于单行文本。
多行省略号老牌的写法是:
.multiline-ellipsis { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }这个写法虽然带-webkit-前缀,但目前在Chrome、Safari、Edge、Firefox里都支持,实际项目中足够用。它的核心是-webkit-line-clamp控制显示行数,中间的-webkit-box-orient: vertical让元素变成垂直方向的弹性容器。唯一要注意的是,容器需要设置宽度,否则现代浏览器会根据自己的content宽度计算换行结果,容易出现省略号出现位置不符合预期。
关于省略号,还有一个实战技巧:如果你希望省略号前面显示“查看详情”这样的固定尾巴,不要用复杂的字符串拼接,直接用伪元素覆盖在右上角,或者用flex的min-width: 0加内联样式,让省略号的文本容器自适应。这个场景在移动端列表里经常出现,后面可以单独开一篇专门讲。
3. 高颜值进阶:字体渐变、自定义字体与细节打磨
3.1 文字渐变与背景裁剪:标题不再单调
文字渐变是现在我给页面标题“上妆”最常用的手法。它的核心并不是某个字体属性,而是把background-clip设为text,让背景色只渲染在文字形状内部,同时把文字本身颜色设为透明:
.gradient-text { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }这段代码需要在意两个兼容点:background-clip: text的标准写法虽然已经在现代浏览器普及,但不少版本还是要求带-webkit-前缀,所以两个都要写;另外color: transparent只有在支持background-clip: text时才能让文字“透出”背景渐变,如果浏览器不支持,文字会直接消失,这很危险。
做好降级很简单,用@supports包一层:
.title { color: #764ba2; } @supports (background-clip: text) or (-webkit-background-clip: text) { .title { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } }这样不支持的浏览器至少能看到一个纯色标题。实际使用中,渐变文字最适合Logo、主标题、数字强调,还可以配合背景图片做“图片文字”,比如让一张风景照出现在文字内部。注意背景图片不要选得过于花哨,否则文字识别度急剧下降。
文字描边效果也可以和渐变搭配,但-webkit-text-stroke会让笔画变细,某些字体在浅色底上反而看不清。我一般只是用它做1px以内的极细描边,模拟一种扁平设计感。
3.2 @font-face 与 woff2:把选中的字体“装”进网页
如果系统自带的字体无法满足视觉要求,就得用@font-face加载真正的自定义字体。基本结构不复杂:
@font-face { font-family: "MyDisplayFont"; src: url("fonts/MyDisplayFont.woff2") format("woff2"), url("fonts/MyDisplayFont.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; }这里有几个关键点。首先是字体格式,现代浏览器已经全面支持woff2,它的压缩率最好,应该作为首选;woff作为兼容备选;ttf文件体积大,能不用就不用。其次是font-display,我建议设置swap。它能让文字先以系统默认字体渲染,等自定义字体加载完成后再替换,避免页面出现长时间空白。但swap也有代价,字体替换瞬间可能会引起布局轻微跳动,如果页面对这种跳动敏感,可以使用optional,让浏览器根据网络情况快速决定是否加载这个字体,但这需要按项目评估。
真正的大坑在中文自定义字体。一个完整的中文字体文件动不动就是几MB甚至十几MB,直接普通加载整个@font-face,页面性能会爆炸。解决办法有两个方向:一是用字体子集化工具,把页面里用到的几百个字抽成一个很小的字体文件,很多生成器的思路都是这个;二是干脆不要全站用自定义中文标题字体,只在少数几个重要数字、LOGO上用,甚至把文字做成SVG图片。前端开发里“性能优先于完美”是一条铁律,尤其在移动端。
@font-face的字体名也要小心,不要和系统已有字体重名。你自定义的字体如果叫PingFang SC,会和macOS系统字体产生命名冲突。我之前见过有人把字体命名为“HarmonyOS Sans”,结果在某些设备上加载出现古怪的渲染。给自定义字体起一个足够特殊的前缀,比如MyFE-Display,能从源头避开很多奇怪问题。
3.3 vertical-align:行内元素对齐的精细调节
vertical-align是行内排版的小瑞士军刀,也是很多新人搞不懂为什么图片底部总多一块空隙的根源。它只对行内元素、内联块级元素和表格单元格生效,对块级元素里的普通文本没有直接作用。
最常见的用法是让图片和文字对齐:
.icon { vertical-align: middle; }middle会让元素的中线对齐父级基线上方半个x的高度,这个解释起来很绕,但视觉效果就是更自然。我们经常看到img底部会多出几像素空白,那是因为默认是基线对齐,而基线下面还有降部空间。用block的图片则没有这个问题,或者你也可以把img的vertical-align: middle设置上,空隙就消失了。
vertical-align的其他值还包括top、bottom、text-top、text-bottom,适合做装饰性图标跟随文字顶线或底线对齐。数字和中文混排时,用vertical-align: baseline可能不太舒服,我会改为middle视觉更稳。表格单元格里,vertical-align: middle则是实现单元格内容垂直居中最好的方式。
这里有一个经典坑:两个inline-block元素之间为什么总有间隙?间隙来自HTML源码换行所产生的空白字符,解决方法是父元素font-size: 0,再给子元素设置回文字字号。但这样会影响整体可维护性,如果只是为了对齐,优先检查是不是vertical-align设置不当造成的“假间隙”,再决定是否重置字号。
4. 踩坑实录:字体冲突、渲染差异与单位迷思
4.1 为什么我写的font-family在别人电脑上变了样
这是前端工位出现频率最高的问题之一。设计稿里的字体是造字工房,你项目里写了一模一样的font-family,结果在同事电脑上打开,发现变成系统默认黑体。原因就是对方电脑上没有安装这个字体。font-family只是给浏览器一个“心愿列表”,如果找不到,就会继续往后的备用字体走,最后用系统兜底。
解决这个问题的思路有三种。第一,使用系统字体栈,接受不同平台间的显示差异,把页面做好的关键是选好回退字体,比如中文页面在Mac上显示苹方、在Windows上显示微软雅黑,这种差异可以接受。第二,把设计稿里的特殊字体通过@font-face打包进网页,任何人打开都能看到一样的效果,代价是性能和加载成本。第三,通过DevTools检测:打开开发者工具,选中元素,在Computed面板检查font-family的实际计算值,看看当前到底落到哪个字体上,再针对性调整fallback顺序。
这里我还想提醒一个细节:本地测试时,如果开发者的电脑安装了设计稿里的字体,页面看起来很正常;但用户电脑没有,就会回退到备选字体,导致样式“看起来不对”。所以在本地验收样式时,可以临时用无痕模式或者切换浏览器user agent来模拟其他系统,或者直接让设计稿字体走web font方案,保证两端稳定。
4.2 中文字体、系统字体与缓存:兼容性背后的真相
中文字体在CSS里的处理比英文字体更敏感。首先,中文字体名称要用引号包住,尤其类似“Microsoft YaHei”这种带空格的名称,不引号可能导致解析异常。其次,中文网页的字体文件体积天生比英文大,因为常用汉字至少两三千个,做web font时要做好子集化,否则一次请求几十万字体的场景会非常酸爽。
还有一个容易忽略的问题是字体缓存。自定义字体通过@font-face加载之后,在用户浏览器里是有缓存的。如果你改了字体文件内容但文件名没变,旧用户可能还看到旧字体,反复反馈“没更新”。解决办法是给字体文件加版本参数:url("fonts/my-font.woff2?v=20250120"),或者让构建工具在打包时自动在文件名里加hash。这和后端清缓存的思路一样,只是很多前端新手容易漏掉。
中文字体之间的“冲突”也值得留意。有些系统预装了仿宋、楷体、黑体等许多字体,如果你的CSS里写了font-family: "仿宋", "楷体", serif;,在不同系统上可能得到完全不同的字形。更稳妥的做法是明确指定某一个字体,并且接受它不存在时回退到serif而不是sans-serif,否则字体会变得不伦不类。实际项目中,常用中文字体栈我会建议直接沿用我前面给出的系统栈,把特殊字体留给少数需要强调的元素。
4.3 单位迷思:文本排版中rem、em、%、vw怎么选
单位问题在文本排版里尤其容易混乱。我做一个简单对照表,方便查阅:
| 单位 | 参照物 | 适用场景 | 注意 |
|---|---|---|---|
| px | 设备像素 | 设计稿精确对齐、边框、投影 | 用户浏览器字号缩放不生效 |
| rem | html根元素字号 | 全局正文字号、间距、组件尺寸 | 根字号被写死时失去无障碍意义 |
| em | 当前元素或父级字号 | 按钮内边距、局部相对缩放 | 嵌套过多会叠加,难预估 |
| % | 父级元素对应属性 | 宽度、高度、行高百分比 | 用于字号时等效于em |
| vw/vh | 视口宽度/高度 | 全屏布局、响应式大标题 | 极端窄屏下可能过小,需要clamp |
vw用在文本字号上有一种很爽的响应式效果,比如font-size: 5vw,文字大小会跟随视口宽度变化。但问题是,在超宽屏上字会大到失控,在很窄的折叠屏上又会显得局促。所以更稳的写法是配合clamp(),我前面已经提过:font-size: clamp(14px, 2vw + 10px, 22px)这种式子能同时提供下限和上限。
关于无障碍,有一条值得深入理解:浏览器里的“字体大小”设置,本质上是修改默认字号,而这个默认字号会传导到html根元素的16px这个基准上。如果你在代码里把根元素font-size写死成14px,用户怎么调都没有用;如果根元素保持100%或者不写,那整个rem体系就能跟着用户偏好缩放。所以现在项目中,我写全局文字大小已经很少用px了,正文全部走rem,特殊参数走clamp,这样既稳定又更包容。
最后再说一个很多人踩过的坑:line-height使用百分比的继承问题。如果父级line-height: 160%,它会被计算成具体像素值再被子元素继承,子元素字号一大,行高就可能偏小甚至重叠。而line-height: 1.6继承的是一个数字,子元素会基于自己字号重新算行高,这是最可靠的。写行高的时候,尽量养成用无单位数字的习惯,耐得住继承链的考验。