1. 为什么这几个单位值得单独拎出来讲做前端这些年我被问得最多的问题里“px、em、rem、%、vh、vw到底啥区别”绝对排得进前三。刚入行的同学经常是写页面时随手抓一个单位就用能跑就行等到做响应式或者换个大屏设备一看布局全乱套了。更麻烦的是有些问题不是“写错了”而是“写对了但选错了单位”这种坑排查起来特别费劲。这篇文章我想把这件事彻底讲透。不是那种列个表格告诉你“px是绝对单位、rem是相对单位”就完事的科普而是从实际项目出发讲清楚每个单位背后的计算逻辑、适用场景、以及我在真实项目里踩过的坑。特别是最近有个热搜词挺有意思——“el-table width min-width可以不带pxem 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就是2rem14px就是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可以不带pxem 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移动端适配主流有两套方案remJS和纯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的坑我一般推荐方案2flex布局更现代也更可控。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相对父元素累积改用rem100vh移动端跳动地址栏影响视口高度用--vh变量JS100vw出现横向滚动包含滚动条宽度改用100%或overflow-x: hiddenel-table列宽rem失效组件只认数字/px用数字JS动态计算rem在小屏上字太小根字号过小font-size用max()设下限padding百分比高度不对参照的是宽度不是高度这是特性用padding-top做占位6. 我个人的单位使用决策树写了这么多最后分享下我自己的决策逻辑基本是条件反射级别的字号正文标题用rem图标和辅助小字用px。用户可访问性优先。间距大间距16px用rem小间距用px。保持呼吸感的同时避免细节失控。宽度流式布局用%或flex全屏用100%需要随视口缩放用vw。高度尽量用min-height全屏用--vh变量固定高度用px。边框永远用px1px就是1px不要动。圆角小组件用px大卡片可以用rem跟着字号缩放。组件库尺寸属性先查文档没写支持的单位一律用数字或px。这套逻辑不是死的具体项目还要看设计规范和适配需求。但有了这个基础框架至少不会犯“用em做全局字号导致嵌套爆炸”这种低级错误。单位选择这件事说到底就是搞清楚每个单位的参照物是什么然后根据场景选最合适的参照物。px参照物理像素em参照父元素字号rem参照根字号%参照父元素对应属性vh/vw参照视口。把这层关系理清了用哪个单位就是自然而然的事不需要死记硬背。我在实际项目里踩过的最大一个坑是在一个后台系统里混用了rem和px做间距结果用户调整浏览器字号后rem间距变了px没变整个页面节奏全乱。后来统一了规范同一个页面里间距单位要么全rem要么全px不要混。这个教训分享出来希望大家少走弯路。
