Search code examples
svgcss-grid

SVGs in a CSS Grid with 0px gap displays background color


SVGs take up 100% of the a CSS-Grid cell (with gap:0px)

But (only in Firefox!) sometimes a white gap is displayed:

  • The unwanted 'gap' shows the Grid background. (there is a gap:0px on the Grid!)

  • Setting shape-rendering:crispEdges makes it an even bigger gap.

  • Setting a fixed integer Grid width (columns * N) pixels 'fixes' the unwanted gap
    (but I can't use that, I have different Grid sizes, and use the available viewPort for the whole Grid)

  • Setting a background color on all Grid cells, does not display that gap line.

  • An extra <rect width='100%' wheight='100%' fill='blue'/> in each SVG does show extra more unwanted gaps.

  • there is also an extra line/gap on the right hand side of the grid.

Does it require an SVG or CSS Grid fix?

Update 1

.. fine in Chrome.

In FireFox red background lines are displayed. 102% overflow 'fix' from answer is applied on mouseover. (but this does stretch the SVG vertically)

<style>
  body {
    --width: 177px; /* simulating responsive error lines */
    --Xwidth: 180px; /* N x 10px 'fixes' the unwanted gaps */
    --overflow: 100%; /* see suggested 'fix' below */
    background: darkslategrey;
  }
  #Tiles {
    width: var(--width);height: var(--width);  
    display: grid;
    grid-template-columns: repeat(10, 1fr);
    grid-template-rows: repeat(10, 1fr);
    gap: 0px; background: red;    /* should not be visible! */
    box-sizing: border-box;
    border: 2px solid yellow;
    overflow:hidden;
  }
  #Tiles:hover{ --overflow: 102%  }
  svg { width: 100%;height: 100%;  vertical-align: top  }
  /* "fix" still leaves one px line at the top. and stretches the SVG vertically */
  svg {
    width: var(--overflow);height: var(--overflow); overflow: visible;
    margin-top: 0px; /* -1px to 'fix' top line */
  }
</style>
<div id=Tiles></div>
<script>
  Tiles.innerHTML = `<my-tile></my-tile>`.repeat(100);
  customElements.define('my-tile', class extends HTMLElement {
    connectedCallback() {
      this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8">` +
        `<rect width='100%' height='100%' fill='blue' /></svg>`;
    }
  });
</script>

Update 2

Resized the SO Snippet to smaller size... now Chrome goes wrong too

That leaves only one fix.. calculate cell-width and round to whole pixels...


Solution

  • You are running into a rounding versus antialiasing trap: Your grid has 10 rows and ten columns, over a total width of 504px. Sizing with repeat(1fr) means each cell gets a width of 50.4px. The SVG gets sized to that value, with the <rect> filling the complete viewport.

    This means that the outline of the grafical element does not coincide with full pixels, and the outermost pixel in each direction gets a partial transparency.

    In principle, the attempt to improve the display with shape-rendering: crispEdges or shape-rendering: geometricPrecision might yield results, but that is not guaranteed. However you divy up 50.4px, either there can be at least one side with an antialiasing artefact, or the browser might round the number of pixels down on some rows or columns.

    The end verdict is always the same:

    The shape-rendering property provides a hint to the implementation about what tradeoffs to make as it renders vector graphics elements...

    Hints are not rules. Every browser is at large to implement its own solution.

    Sizing the container

    Making sure elements have full pixel sizes is one solution, and should always be the first choice. If you have a grid of ten elements per dimension, you would have to make sure the grid container has always a multiple of 10px size.

    But if you want your grid to be responsive, this might prove complicated. Only media queries can achieve that. (The CSS calc() function only accepts the four basic operators and will not round values.)

    Cheating the rounding function

    But there is also another way to go. Some overlap between neighbouring <svg> elements can be achieved with an overflow: visible, so that content outside the viewBox is not clipped, and increasing the area of rendered content. This does not change the computation of box sizes, it only means the implicit clip path surrounding the <svg> element is removed.

    This obviously errs to the opposite side, and if it is critical that the outermost pixel is diplayed correctly, this might be just the wrong solution. But for your example, it improves the result.

    The choosen size of the width of 102% is enough to cover 1px if the svg is diplayed on screen at a size of ~50px. Obviously, this value might be the wrong one for other scales, but that is the nature of drawing vector graphics onto a screen divided into pixels.

    div {
      position: relative;
      height: 51px;
      width: 101px;
      margin: 20px;
      background: red;
    }
    svg {
      width: 50.4px;
      height: 50.4px;
    }
    rect {
      fill: blue;
    }
    path {
      stroke: white;
    }
    .overflowing svg {
      overflow: visible;
    }
    <div>
      <svg viewBox="0 0 8 8">
        <rect width='100%' height='100%' />
        <path d="M4 0v2"/><path d="M6 4h2"/>
        <path d="M4 6v2"/><path d="M0 4h2"/>
      </svg><svg viewBox="0 0 8 8">
        <rect width='100%' height='100%' />
        <path d="M4 0v2"/><path d="M6 4h2"/>
        <path d="M4 6v2"/><path d="M0 4h2"/>
      </svg><br/>
    </div>
    <div class="overflowing">
      <svg viewBox="0 0 8 8">
        <rect width='102%' height='102%' />
        <path d="M4 0v2"/><path d="M6 4h2"/>
        <path d="M4 6v2"/><path d="M0 4h2"/>
      </svg><svg viewBox="0 0 8 8">
        <rect width='102%' height='102%' />
        <path d="M4 0v2"/><path d="M6 4h2"/>
        <path d="M4 6v2"/><path d="M0 4h2"/>
      </svg>
    </div>