Search code examples
htmlcsssvgretina

How to prevent half-pixel SVG shift on high pixel ratio devices (Retina)?


I have a HTML webpage with SVG image. I get a problem (excess white line, shown on the picture below) on the webpage when I visit it using iOS Safari or Android Browser. The screenshot resolution is 2x, the saw edge is a SVG image.

Expectation vs. result

I've found out that it happens when the page Y-position of the SVG image is not an integer amount of CSS pixels (px), i.e. with ½px. The browser rounds the SVG image position to integer px when it renders the webpage while doesn't round the other elements positions. That's why the ½px line appears.

Explanation

You can reproduce the problem using the snippet below (or this CodePen). You should run the snippet on a device with a high pixel density. You can also reproduce it in desktop Safari if you go to the responsive design mode and pick iPhone or iPad.

.common-bg {
  background: #222;
  fill: #222;
}
.block {
  max-width: 300px;
  margin: 20px auto;
}
.block_content {
  height: 50.5px;
}
.block_edge {
  display: block;
}
<div class="block">
  <div class="block_content common-bg"></div>
  <svg
    class="block_edge"
    width="100%"
    height="10"
    xmlns="http://www.w3.org/2000/svg"
    version="1.1"
    xmlns:xlink="http://www.w3.org/1999/xlink"
  >
    <defs>
      <pattern id="sawPattern" x="50%" width="20" height="10" patternUnits="userSpaceOnUse">
        <path d="M 0 0 L 10 10 L 20 0 Z" class="common-bg"/>
      </pattern>
    </defs>
    <rect x="0" y="0" width="100%" height="10" fill="url(#sawPattern)"/>
  </svg>
</div>

How to prevent ½px SVG shift on iOS Safari and Android Browser? Is it a bug and I should report it to WebKit developers? Maybe there is a way to make browsers round to px the other elements on the page?

I can solve this problem without preventing ½px shift:

  • Remove non-integer height of .block_content
  • Make such layout in which half-pixel shift doesn't lead to while line

But I wonder is there a way to prevent ½px shift because the solutions above are not always possible.


Solution

  • iOS: You just need to add any CSS transform to the SVG element to fix it in Safari. For example .block_edge {-webkit-transform: scale(1); transform: scale(1)}.

    Android: First you need to add a tiny CSS scale transform to the SVG element. When you do it, the <svg> and the <rect> elements will be rendered where they must be but the <rect> background will be repeated at the top and at the bottom:

    enter image description here

    To fix it you need to extend the pattern to the top and the bottom to prevent background repeating. Then you need to add a filled <rect> just above the top of the SVG to remove the last blank line at the top. There still will left a hardly visible dark grey line at the top in Android browser.

    .common-bg {
      background: #222;
      fill: #222;
    }
    .block {
      max-width: 300px;
      margin: 20px auto;
    }
    .block_content {
      height: 50.5px;
    }
    .block_edge {
      display: block;
      
      /* Fix. No more than 5 zeros. */
      -webkit-transform: scale(1.000001);
      transform: scale(1.000001);
    }
    <div class="block">
      <div class="block_content common-bg"></div>
      <svg
        class="block_edge"
        width="100%"
        height="10"
        xmlns="http://www.w3.org/2000/svg"
        version="1.1"
        xmlns:xlink="http://www.w3.org/1999/xlink"
      >
        <defs>
          <pattern id="sawPattern" x="50%" y="-1" width="20" height="12" patternUnits="userSpaceOnUse">
            <path d="M 0 0 L 0 1 L 10 11 L 20 1 L 20 0 Z" class="common-bg"/>
          </pattern>
        </defs>
        <rect x="0" y="-1" width="100%" height="1" common-bg="common-bg"/>
        <rect x="0" y="0" width="100%" height="10" fill="url(#sawPattern)"/>
      </svg>
    </div>

    The snippet on CodePen

    I tested it on mobile and desktop Safari 10, Android 4.4 and Chrome 58 on Android.

    Conclusion: the fixes are too complicated and not reliable so I advice to make such layout in which half-pixel shift doesn't lead to a blank line.

    .common-bg {
      background: #222;
      fill: #222;
    }
    .block {
      max-width: 300px;
      margin: 20px auto;
    }
    .block_content {
      height: 50.5px;
    }
    .block_edge {
      display: block;
      
      /* Overflow for unexpected translateY */
      margin-top: -1px;
    }
    <div class="block">
      <div class="block_content common-bg"></div>
      <svg
        class="block_edge"
        width="100%"
        height="12"
        xmlns="http://www.w3.org/2000/svg"
        version="1.1"
        xmlns:xlink="http://www.w3.org/1999/xlink"
      >
        <defs>
          <!-- The teeth pattern is extended to the top -->
          <pattern id="sawPattern" x="50%" width="20" height="12" patternUnits="userSpaceOnUse">
            <path d="M 0 0 L 0 1 L 10 11 L 20 1 L 20 0 Z" class="common-bg"/>
          </pattern>
        </defs>
        <rect x="0" y="0" width="100%" height="11" fill="url(#sawPattern)"/>
      </svg>
    </div>

    The snippet on CodePen