1px 问题
约 1171 字大约 4 分钟
2025-07-04
问题原因
移动端 1px 问题指的是在 Retina 等高 PPI 屏幕上,CSS 设置的 1px 边框看起来比实际更粗的现象。原因在于:
- 设备像素比(DPR)差异:Retina 屏幕的物理像素密度是普通屏幕的 2 倍或 3 倍
- CSS 像素与物理像素的映射:1 个 CSS 像素可能对应多个物理像素
- 浏览器渲染机制:不同浏览器对亚像素(0.5px)渲染处理方式不同
传统解决方案
1、使用 0.5px
.border {
border: 0.5px solid #000;
}
缺点:兼容性差,iOS8+和 Android4.4+才支持
2、使用伪类 + transform 缩放
.border {
position: relative;
}
.border::after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid #000;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
}
缺点:实现复杂,可能影响子元素定位
3. 使用 viewport 缩放
通过 JavaScript 动态修改 viewport 的 initial-scale 值:
<meta
name="viewport"
id="viewport"
content="width=device-width,initial-scale=1,user-scalable=no"
/>
<script>
const viewport = document.getElementById("viewport");
if (window.devicePixelRatio === 2) {
viewport.setAttribute(
"content",
"width=device-width,initial-scale=0.5,user-scalable=no"
);
} else if (window.devicePixelRatio === 3) {
viewport.setAttribute(
"content",
"width=device-width,initial-scale=0.333333,user-scalable=no"
);
}
</script>
缺点:全局影响,需要重新计算所有尺寸
4. 使用 background-image 渐变
.border {
background-image: linear-gradient(0deg, #000 50%, transparent 50%);
background-size: 100% 1px;
background-repeat: no-repeat;
background-position: bottom;
}
缺点:只能实现单边边框,圆角支持差
现代处理方案
提示
现代很多主流的框架都内知道 1px 的处理,无需自己实现。比如 uniapp 的 1rpx
1. 使用 CSS 的 border-image
.border {
border-width: 1px;
border-image: url("data:image/png;base64,...") 2 stretch;
}
2. 使用 box-shadow 模拟
.border {
box-shadow: 0 0 0 0.5px #000;
}
优点:简单易用,支持圆角 缺点:不支持复杂的边框样式
3. 使用 SVG 绘制
.border {
background: url("data:image/svg+xml;utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><rect width='100%' height='100%' fill='none' stroke='black' stroke-width='1'/></svg>");
}
4. 使用 CSS 的 min-device-pixel-ratio 媒体查询
@media (-webkit-min-device-pixel-ratio: 2) {
.border {
border-width: 0.5px;
}
}
5. 使用 postcss-write-svg 插件
@svg 1px-border {
height: 2px;
@rect {
fill: var(--color, black);
width: 100%;
height: 50%;
}
}
.border {
border: 1px solid transparent;
border-image: svg(1px-border param(--color #000)) 2 2 stretch;
}
6. sass + transform + css 伪类
.scale-hairline-common(@color, @top, @right, @bottom, @left) {
content: '';
position: absolute;
background-color: @color;
display: block;
z-index: 1;
top: @top;
right: @right;
bottom: @bottom;
left: @left;
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'top') {
border-top: 1PX solid @color; // PX 大写防止被 postcss 转换
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-top: none;
&::before {
.scale-hairline-common(@color, 0, auto, auto, 0);
width: 100%;
height: 1PX; // PX 大写防止被 postcss 转换
transform-origin: 50% 50%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx) {
transform: scaleY(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'right') {
border-right: 1PX solid @color;
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-right: none;
&::after {
.scale-hairline-common(@color, 0, 0, auto, auto);
width: 1PX; // PX 大写防止被 postcss 转换
height: 100%;
background: @color;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx) {
transform: scaleX(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'bottom') {
border-bottom: 1PX solid @color; // PX 大写防止被 postcss 转换
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-bottom: none;
&::after {
.scale-hairline-common(@color, auto, auto, 0, 0);
width: 100%;
height: 1PX; // PX 大写防止被 postcss 转换
transform-origin: 50% 100%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx) {
transform: scaleY(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base) when (@direction = 'left') {
border-left: 1PX solid @color; // PX 大写防止被 postcss 转换
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
border-left: none;
&::before {
.scale-hairline-common(@color, 0, auto, auto, 0);
width: 1PX; // PX 大写防止被 postcss 转换
height: 100%;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx) {
transform: scaleX(0.33);
}
}
}
}
}
.hairline(@direction, @color: @border-color-base, @radius: 0) when (@direction = 'all') {
border: 1PX solid @color; // PX 大写防止被 postcss 转换
border-radius: @radius;
html:not([data-scale]) & {
@media (min-resolution: 2dppx) {
position: relative;
border: none;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border: 1PX solid @color; // PX 大写防止被 postcss 转换
border-radius: @radius * 2;
transform-origin: 0 0;
transform: scale(0.5);
box-sizing: border-box;
pointer-events: none;
// @media (min-resolution: 3dppx) {
// width: 300%;
// height: 300%;
// border-radius: @radius * 3;
// transform: scale(0.33);
// }
}
}
}
}
主流处理方案
1. PostCSS 插件
比如postcss-px-to-viewport
,1px 会按照设计稿 750px 的宽度转换为 0.13333vw
module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px",
viewportWidth: 750, // 设计稿宽度
unitPrecision: 5,
propList: ["*"], // 所有属性转换
viewportUnit: "vw",
fontViewportUnit: "vw",
selectorBlackList: [],
minPixelValue: 1, // 1px问题
mediaQuery: false,
replace: true,
exclude: [/node_modules/],
},
},
};
2、小程序/Weex 原生支持 0.5px
.border {
border: 0.5px solid #000; /* 直接支持 */
}
最佳实践建议
- 简单场景:优先使用
box-shadow
方案 - 复杂边框:考虑使用 SVG 或 border-image
- 框架项目:可使用 postcss 插件自动处理
- 新项目:评估是否可以使用 CSS 的
border-width: thin
等新特性
随着浏览器支持度提升,这些方案会逐渐简化,但目前仍需根据项目需求选择最合适的解决方案。
更新日志
2025/8/24 08:17
查看所有更新日志
e7112
-1于