1. 为什么这几个单位值得单独拎出来讲
做前端这些年,我被问得最多的问题里,“px、em、rem、%、vh、vw到底啥区别”绝对排得进前三。刚入行的同学经常是写页面时随手抓一个单位就用,能跑就行,等到做响应式或者换个大屏设备一看,布局全乱套了。更麻烦的是,有些问题不是“写错了”,而是“写对了但选错了单位”,这种坑排查起来特别费劲。
这篇文章我想把这件事彻底讲透。不是那种列个表格告诉你“px是绝对单位、rem是相对单位”就完事的科普,而是从实际项目出发,讲清楚每个单位背后的计算逻辑、适用场景、以及我在真实项目里踩过的坑。特别是最近有个热搜词挺有意思——“el-table width min-width可以不带px,em px失效”,这背后其实牵扯到组件库对单位值的解析机制,我也会在文章里专门拆解。
不管你是刚学CSS的新手,还是写了几年页面但一直没系统梳理过单位体系的老手,这篇内容应该都能帮你把这块知识补扎实。核心关键词就几个:px、em、rem、vh、vw,外加百分比%,我会把它们放在同一个坐标系里对比,让你看完之后能形成条件反射——什么场景该用什么单位,心里有数。
2. 六个单位的本质区别:先搞懂“相对谁”
2.1 px:最老实但也最容易被误用的单位
px是像素单位,在大多数人的认知里它就是“固定大小”。这个理解对,但不完全对。CSS里的px严格来说是一个逻辑像素,它和物理屏幕上的发光点不是一回事。在高分屏(比如Retina屏)上,1个CSS像素可能对应2个甚至3个物理像素点,这是操作系统和浏览器做的缩放处理。
为什么这个细节重要?因为很多人以为px是“绝对单位”,所以在做响应式时完全不敢用。但实际上,px在边框、阴影、小图标尺寸这些场景下依然是最优选择。你想想,一个1px的边框,如果用rem去写,用户改了浏览器字号,边框跟着变粗变细,视觉上反而奇怪。
px真正的问题在于页面整体缩放场景。比如你写了一个width: 1200px的容器,在手机屏幕上直接溢出。这时候不是px的错,是你选错了工具。
注意:
px在浏览器缩放(Ctrl+加减号)时也会跟着缩放,它并不是完全“死”的。真正“死”的是物理像素,但那个层面开发者一般不需要关心。
2.2 em:相对父元素字号,嵌套时容易翻车
em的计算规则是:相对于当前元素的font-size。如果当前元素没有设置font-size,就继承父元素的font-size。
这里有个经典陷阱:em用在font-size属性本身时,参照的是父元素的font-size;用在其他属性(如width、padding)时,参照的是当前元素的font-size。这个区别很多人搞混。
举个例子:
.parent { font-size: 16px; } .child { font-size: 1.5em; /* 16 * 1.5 = 24px */ padding: 1em; /* 24 * 1 = 24px,注意这里参照的是child自己的24px */ }嵌套多层时,em会逐层累积。如果每层都写1.2em,三层下来就是1.2 * 1.2 * 1.2 = 1.728倍,字号直接失控。这就是为什么很多团队在项目规范里直接禁用em做字号单位。
但em也不是一无是处。它最适合的场景是组件内部的等比缩放。比如一个按钮组件,你希望padding、border-radius、图标大小都跟着按钮文字大小等比变化,那用em就非常合适——改一个font-size,整个组件比例协调地缩放。
2.3 rem:相对根元素,响应式的利器
rem的r就是root的意思,它只相对于根元素<html>的font-size,不受父元素影响。这就解决了em嵌套累积的问题。
默认情况下,浏览器根字号是16px,所以1rem = 16px。但你可以通过设置html { font-size: 62.5%; }把根字号变成10px,这样1rem = 10px,换算起来特别方便——设计稿上20px就是2rem,14px就是1.4rem。
rem最经典的用法是移动端适配。配合一段JS动态设置根字号:
function setRem() { const docEl = document.documentElement; const width = docEl.clientWidth; // 以375px设计稿为基准,1rem = 100px(方便计算) const rem = width / 375 * 100; docEl.style.fontSize = rem + 'px'; } window.addEventListener('resize', setRem); setRem();这样设计稿上量到多少px,除以100就是rem值,页面会随屏幕宽度等比缩放。这套方案在移动端H5页面里用了很多年,非常成熟。
提示:
rem做移动端适配时,记得给根字号设一个最大值,否则在iPad或者折叠屏上字会大到离谱。通常配合媒体查询限制在50px左右封顶。
2.4 %:最灵活也最需要看“参照物”的单位
百分比的核心在于它相对谁计算。这个问题不搞清楚,用%就是灾难。
width的%相对于父元素的content widthheight的%相对于父元素的content height(如果父元素高度是auto,那子元素height的%会失效)padding和margin的%,无论上下左右,都相对于父元素的widthfont-size的%相对于父元素的font-sizeline-height的%相对于当前元素的font-size
看到没,同样是%,参照物完全不同。特别是padding的上下百分比也参照宽度这一点,很多人不知道,但做等比占位图时特别有用——设置padding-top: 56.25%就能撑出一个16:9的容器。
%最适合流式布局,比如左右两栏按比例分配宽度。但它的局限是必须依赖父元素有明确尺寸,父元素尺寸不确定时,%就不可靠。
2.5 vh和vw:相对视口,大屏适配的救星
vh和vw是视口单位,1vh = 视口高度的1%,1vw = 视口宽度的1%。注意这里的“视口”在移动端有坑——浏览器地址栏收起和展开时,视口高度会变化,导致100vh的元素高度跳动。
这个问题在移动端很常见,解决方案是用100dvh(动态视口高度)或者用JS动态计算设置。不过dvh的兼容性在旧设备上一般,实际项目里我通常还是用JS兜底。
vh/vw最典型的应用是全屏首屏:
.hero { width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; }但要注意100vw在有些浏览器里会包含滚动条宽度,导致出现横向滚动条。稳妥的做法是用width: 100%代替100vw,或者给body加overflow-x: hidden。
2.6 六个单位横向对比速查表
| 单位 | 参照物 | 是否继承影响 | 典型场景 | 主要坑点 |
|---|---|---|---|---|
| px | 无(逻辑像素) | 否 | 边框、阴影、小图标 | 大屏适配不灵活 |
| em | 父/当前元素font-size | 是,会累积 | 组件内等比缩放 | 嵌套时字号失控 |
| rem | 根元素font-size | 否 | 移动端适配、全局缩放 | 需配合JS或媒体查询 |
| % | 父元素对应属性 | 视属性而定 | 流式布局、等比占位 | 参照物不明确易出错 |
| vh | 视口高度 | 否 | 全屏区块 | 移动端地址栏导致跳动 |
| vw | 视口宽度 | 否 | 全屏宽度、字体缩放 | 可能包含滚动条宽度 |
这张表建议截图存手机里,写页面拿不准的时候翻出来看一眼,比翻文档快。
3. 实际项目里怎么选:场景驱动的决策逻辑
3.1 字号体系:rem为主,px兜底
字号是我最建议用rem的地方。原因很简单:用户可能在浏览器里调整了默认字号(比如老年人把字号调到20px),如果你用px写死,用户调了也没用,体验很差。用rem的话,用户调大根字号,整个页面文字等比放大,可访问性直接拉满。
但也不是所有字号都用rem。图标字体或者需要精确对齐的小字号,我一般还是用px。比如12px的辅助文字,用rem写就是0.75rem,换算麻烦不说,用户把根字号调到20px时变成15px,可能就破坏了原本紧凑的布局。
我的实践方案是:正文和标题用rem,辅助性小字和图标用px。这样既保证了可访问性,又不会让细节失控。
3.2 间距体系:rem和px混用有讲究
padding、margin这些间距,我的习惯是大间距用rem,小间距用px。比如卡片之间的24px间距用1.5rem,这样用户缩放时整体呼吸感保持一致;但按钮内部的8px小间距用px,避免缩放后按钮变形。
这里有个经验:间距的rem值最好用偶数,比如0.5rem、1rem、1.5rem,对应8px、16px、24px。这样在根字号变化时,间距依然保持整数像素,不会出现半像素导致的模糊。
3.3 布局宽度:%、flex、vw各司其职
页面主布局我基本不用固定宽度。两栏布局用%或者flex,比如侧边栏width: 25%,主内容区flex: 1。全屏banner用100vw,但记得处理滚动条问题。
有个细节:flex布局里的flex-basis如果用%,参照的是父容器主轴尺寸,这个和width的%逻辑一致。但flex-grow和flex-shrink是无单位的比例值,不要和%搞混。
3.4 高度:vh慎用,min-height更稳
高度是我最不建议滥用vh的地方。除了移动端地址栏问题,100vh在桌面端也可能因为浏览器工具栏导致内容被遮挡。我的做法是:首屏用min-height: 100vh而不是height: 100vh,这样内容多了可以撑开,内容少了也能占满屏幕。
如果确实需要精确的全屏高度,用JS读取window.innerHeight然后设置CSS变量,这是最稳的方案:
document.documentElement.style.setProperty('--vh', window.innerHeight * 0.01 + 'px');然后CSS里用calc(var(--vh) * 100)代替100vh。
3.5 组件库里的单位陷阱:el-table的width为什么不带px也行
回到热搜词里提到的“el-table width min-width可以不带px,em px失效”。这个现象的本质是Element UI对width属性的解析逻辑。
el-table的column配置里,width可以写100也可以写'100px'。如果你写100,组件内部会把它当数字处理,最终拼接成100px。但如果你写'100em'或者'100rem',组件可能无法正确解析,因为它内部可能只做了数字到px的转换,或者用parseInt提取数字后重新拼px。
我翻过Element UI的源码,它的处理逻辑大致是:
// 简化后的逻辑 let width = column.width; if (typeof width === 'number') { width = width + 'px'; } // 如果是字符串,直接透传给style所以'100px'能生效,'100em'理论上也能透传,但实际渲染时表格列宽计算可能不认em单位,导致失效。最稳妥的做法是el-table的width统一用数字或者px字符串,不要用其他单位。
这个坑我在一个后台管理系统里踩过,当时想把表格列宽做成响应式的,用了rem,结果列宽完全乱掉。后来改成数字+JS动态计算才解决。
提示:不只是el-table,很多组件库的尺寸属性都只认数字或px。用之前先看文档,文档没写的单位不要想当然。
4. 移动端适配实战:rem方案完整落地
4.1 方案选型:为什么我最终选了rem而不是vw
移动端适配主流有两套方案:rem+JS和纯vw。vw方案看起来更优雅,不需要JS,但有两个硬伤:一是无法设置最大宽度,在iPad上元素会大到离谱;二是无法处理1px边框问题(vw在小屏幕上可能小于1px,浏览器渲染会出问题)。
rem方案虽然需要一段JS,但可以灵活控制根字号的最大值和最小值,适配范围更可控。所以我最终选了rem方案,配合媒体查询做边界限制。
4.2 核心代码:动态根字号计算
(function (doc, win) { const docEl = doc.documentElement; const resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'; function recalc() { let clientWidth = docEl.clientWidth; if (!clientWidth) return; // 限制最大宽度,避免大屏上字太大 if (clientWidth > 750) { clientWidth = 750; } // 以750设计稿为基准,1rem = 100px // 这样设计稿上量到75px,写0.75rem即可 docEl.style.fontSize = 100 * (clientWidth / 750) + 'px'; } if (!doc.addEventListener) return; win.addEventListener(resizeEvt, recalc, false); doc.addEventListener('DOMContentLoaded', recalc, false); recalc(); })(document, window);这段代码的关键点:
- 基准值选100:设计稿上量到多少px,小数点左移两位就是rem值,心算就能完成,不用计算器。
- 最大宽度限制750:超过750px(比如iPad)就不再放大,保持和750设计稿一致的视觉比例。
- orientationchange事件:横竖屏切换时重新计算,这个事件在移动端必须监听。
4.3 设计稿到代码的换算流程
假设设计稿宽度750px,上面有一个卡片:
- 卡片宽度:690px → 6.9rem
- 卡片内边距:30px → 0.3rem
- 标题字号:32px → 0.32rem
- 标题下间距:20px → 0.2rem
换算规则就是px值除以100。我在实际项目里会在IDE里装一个px转rem的插件,选中px值按快捷键直接转换,效率很高。
但要注意:1px边框不要转rem。在750设计稿上1px实际是0.5px物理像素,转成rem后在有些设备上会消失。正确做法是用transform: scaleY(0.5)或者直接写1px配合媒体查询。
4.4 字体大小的特殊处理
rem方案下,字体也会跟着屏幕缩放。这在大多数情况下是好的,但有个问题:小屏幕手机上字会变得很小。比如320px宽的屏幕,根字号是100 * 320 / 750 = 42.67px,原本16px的字变成0.16 * 42.67 = 6.8px,根本看不清。
解决方案是给字体设置最小值:
.text { font-size: max(12px, 0.16rem); }max()函数在现代浏览器里支持度已经很好,这样既保持了rem的缩放能力,又保证了可读性下限。
5. 常见问题排查与避坑指南
5.1 为什么我的height: 100%不生效
这是新手最常遇到的问题。height: 100%生效的前提是父元素有明确的高度。如果父元素高度是auto,子元素的100%就无从计算,浏览器会忽略这个声明。
解决方案有三种:
- 给父元素链上每一层都设置height: 100%,一直到html和body
- 父元素用
display: flex,子元素用flex: 1 - 用
height: 100vh代替(但注意vh的坑)
我一般推荐方案2,flex布局更现代也更可控。
5.2 rem在PC端页面怎么用
PC端页面我一般不用rem做整体缩放,因为PC屏幕尺寸跨度太大,从1366到4K都有,等比缩放会导致大屏上字巨大。PC端更适合固定最大宽度+媒体查询的方案。
但PC端可以用rem做字号可访问性:根字号保持16px不变,用户如果调整浏览器字号,rem写的文字会跟着变。这样既不影响布局,又照顾了特殊用户。
5.3 vh在移动端的跳动问题怎么彻底解决
前面提到了--vh变量的方案,这里给一个完整的实现:
:root { --vh: 1vh; } .fullscreen { height: calc(var(--vh, 1vh) * 100); }function setVh() { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', vh + 'px'); } window.addEventListener('resize', setVh); window.addEventListener('orientationchange', setVh); setVh();这个方案的核心是用JS读取真实视口高度,绕开浏览器地址栏的干扰。实测在iOS Safari和安卓Chrome上都很稳。
5.4 百分比padding做占位图的原理
前面提到padding-top: 56.25%可以做16:9占位,这里解释下原理:padding的百分比无论上下左右都相对于父元素的宽度。所以父元素宽度确定后,padding-top的百分比就能算出一个确定的高度值,从而实现等比。
16:9的比例计算:9 / 16 = 0.5625,所以padding-top: 56.25%。同理4:3就是75%,1:1就是100%。
这个技巧在图片懒加载占位、视频容器上用得非常多,比JS计算高度优雅得多。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| height: 100%不生效 | 父元素高度为auto | 父元素设flex或明确高度 |
| em字号越嵌套越大 | em相对父元素累积 | 改用rem |
| 100vh移动端跳动 | 地址栏影响视口高度 | 用--vh变量+JS |
| 100vw出现横向滚动 | 包含滚动条宽度 | 改用100%或overflow-x: hidden |
| el-table列宽rem失效 | 组件只认数字/px | 用数字+JS动态计算 |
| rem在小屏上字太小 | 根字号过小 | font-size用max()设下限 |
| padding百分比高度不对 | 参照的是宽度不是高度 | 这是特性,用padding-top做占位 |
6. 我个人的单位使用决策树
写了这么多,最后分享下我自己的决策逻辑,基本是条件反射级别的:
字号:正文标题用rem,图标和辅助小字用px。用户可访问性优先。
间距:大间距(>16px)用rem,小间距用px。保持呼吸感的同时避免细节失控。
宽度:流式布局用%或flex,全屏用100%,需要随视口缩放用vw。
高度:尽量用min-height,全屏用--vh变量,固定高度用px。
边框:永远用px,1px就是1px,不要动。
圆角:小组件用px,大卡片可以用rem跟着字号缩放。
组件库尺寸属性:先查文档,没写支持的单位一律用数字或px。
这套逻辑不是死的,具体项目还要看设计规范和适配需求。但有了这个基础框架,至少不会犯“用em做全局字号导致嵌套爆炸”这种低级错误。
单位选择这件事,说到底就是搞清楚每个单位的参照物是什么,然后根据场景选最合适的参照物。px参照物理像素,em参照父元素字号,rem参照根字号,%参照父元素对应属性,vh/vw参照视口。把这层关系理清了,用哪个单位就是自然而然的事,不需要死记硬背。
我在实际项目里踩过的最大一个坑,是在一个后台系统里混用了rem和px做间距,结果用户调整浏览器字号后,rem间距变了px没变,整个页面节奏全乱。后来统一了规范:同一个页面里,间距单位要么全rem要么全px,不要混。这个教训分享出来,希望大家少走弯路。