移动端 1px 像素问题解决方案

解决 Retina 屏幕下 CSS 1px 边框过粗的问题

问题

在 Retina 屏幕(DPR >= 2)上,CSS 的 1px 会被渲染成 2 个或更多物理像素,导致边框看起来比设计稿粗。如何让 1px 在高清屏上显示为真正的 1 物理像素?

解答

方案一:伪元素 + transform(推荐)

最常用的方案,兼容性好:

/* 单边框 */
.border-bottom {
  position: relative;
}

.border-bottom::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: #000;
  transform: scaleY(0.5);
  transform-origin: 0 0;
}

/* 四边框 */
.border-all {
  position: relative;
}

.border-all::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 200%;
  border: 1px solid #000;
  transform: scale(0.5);
  transform-origin: 0 0;
  box-sizing: border-box;
  pointer-events: none; /* 避免遮挡点击 */
  border-radius: 8px; /* 如需圆角,设为实际值的 2 倍 */
}

方案二:根据 DPR 动态设置

使用 JavaScript 检测 DPR,动态调整缩放比例:

// 获取设备像素比
const dpr = window.devicePixelRatio || 1;

// 设置 CSS 变量
document.documentElement.style.setProperty('--dpr', dpr);
document.documentElement.style.setProperty('--border-scale', 1 / dpr);
.border-bottom::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: #000;
  transform: scaleY(var(--border-scale, 0.5));
}

方案三:使用 box-shadow

简单但颜色会变浅:

.border-bottom {
  box-shadow: 0 1px 0 0 #000;
}

/* 0.5px 的阴影在 DPR=2 的屏幕上接近 1 物理像素 */
.border-bottom-thin {
  box-shadow: 0 0.5px 0 0 #000;
}

方案四:使用 SVG border-image

精确控制,但写法复杂:

.border-bottom {
  border-bottom: 1px solid transparent;
  border-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3Crect width='1' height='0.5' fill='%23000'/%3E%3C/svg%3E") 0 0 1 0 stretch;
}

方案五:viewport 缩放

整体缩放页面,需要配合 rem 使用:

<script>
  const dpr = window.devicePixelRatio || 1;
  const scale = 1 / dpr;
  
  // 设置 viewport
  document.querySelector('meta[name="viewport"]').setAttribute(
    'content',
    `width=device-width,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale},user-scalable=no`
  );
  
  // 设置根字体大小,配合 rem 使用
  document.documentElement.style.fontSize = dpr * 100 + 'px';
</script>
/* 使用 rem,1rem = 100px * dpr */
.box {
  width: 1rem; /* 实际 100px */
  border: 1px solid #000; /* 真正的 1 物理像素 */
}

封装通用 Sass Mixin

// 1px 边框 mixin
@mixin border-1px($color: #000, $direction: bottom, $radius: 0) {
  position: relative;
  
  &::after {
    content: '';
    position: absolute;
    pointer-events: none;
    
    @if $direction == bottom {
      left: 0;
      bottom: 0;
      width: 100%;
      height: 1px;
      background: $color;
      transform: scaleY(0.5);
    } @else if $direction == top {
      left: 0;
      top: 0;
      width: 100%;
      height: 1px;
      background: $color;
      transform: scaleY(0.5);
    } @else if $direction == all {
      top: 0;
      left: 0;
      width: 200%;
      height: 200%;
      border: 1px solid $color;
      border-radius: $radius * 2;
      transform: scale(0.5);
      transform-origin: 0 0;
      box-sizing: border-box;
    }
  }
}

// 使用
.list-item {
  @include border-1px(#e5e5e5, bottom);
}

.card {
  @include border-1px(#ddd, all, 8px);
}

关键点

  • 问题根源:DPR > 1 时,1 个 CSS 像素对应多个物理像素,导致边框变粗
  • 主流方案:伪元素 + hd18w scale 缩放,兼容性好,使用灵活
  • 缩放比例:DPR=2 时缩放 0.5,DPR=3 时缩放 0.33
  • 圆角处理:四边框方案中,border-radius 需设为实际值的 2 倍
  • pointer-events:伪元素要设置 pointer-events: none 避免遮挡交互