Skip to content

Space between tiles on fractional zoom levels #3575

Description

@Eschon

Note

This issue now has 150+ comments, most of which are concerned with hacky workarounds. I've compiled a list of a few comments that are discussing potential solutions:

(See also #3575 (comment).)

Relevant browser bugs, filed by @hyperknot:


(Also, mix-blend-mode is now used to alleviate the tile gaps in Chromium-based browsers: #8891.)

/@Malvoz

screen shot 2015-06-30 at 14 39 12

As the screenshot shows there is a small space between the tiles. This happens to me on fractional zoom levels in Safari, Chrome and Opera. It seems like this was fixed in #2377 but later removed in 3ccbe5b in favour of better performance.

I tried reimplementing the fix to see the performance impact myself and it was pretty bad furthermore it didn't even fix the problem for Chrome.

Does anyone have an idea how this can be fixed? Fractional zoom levels would be a great feature for our app but as long as this bug remains we can't really use them.

Edit: I set up a jsFiddle that shows the problem: http://jsfiddle.net/Eschon/pw9jcjus/1/

Activity

  1. mourner commented on Jun 30, 2015

    @mourner
    Member

    No idea. :( Although I only saw the problem in Safari, — Chrome/Opera worked fine.

  2. IvanSanchez commented on Jun 30, 2015

    @IvanSanchez
    Member

    Does anyone have an idea how this can be fixed?

    I have an idea, but it's linked to the words "radical" and "experimental":

    https://github.com/IvanSanchez/Leaflet.gl

    One of the things I discovered is that textures have to be clamped. i.e. if this line is removed : https://github.com/IvanSanchez/Leaflet.gl/blob/master/src/core/GlUtil.js#L74 slivers ("spaces between tiles") appear (as the texture wraps around the triangle and half a pixel of the other side of the tile is shown). So, my educated guess is that Webkit is antialiasing half a pixel on the edge of each image.

    As of now, the tilelayer triangles form a complete mesh that is rendered without slivers... but the words "experimental" will be all over that for the time being.

  3. Eschon commented on Jun 30, 2015

    @Eschon
    ContributorAuthor

    @mourner It has a stronger effect in Safari but it is also visible in Chrome.
    screen shot 2015-06-30 at 15 42 07

    @IvanSanchez Wow nice work, but I don't think that I can use that (yet).

  4. mourner commented on Jun 30, 2015

    @mourner
    Member

    @Eschon yeah, I probably didn't notice this because on Retina screen, it's even less visible.

  5. Eschon commented on Jul 30, 2015

    @Eschon
    ContributorAuthor

    This seems to affect Firefox now too in some cases.
    I couldn't reproduce it in jsFiddle but if you look at that site
    I get the same spaces in Firefox 39 and 41.0a2

    Edit: This seems to be a problem with iframes. If I open the map directly it doesn't reproduce.

  6. added this to the 1.0-beta2 milestone on Sep 28, 2015
  7. modified the milestones: , 1.0-beta2 on Oct 7, 2015
  8. IvanSanchez commented on Oct 7, 2015

    @IvanSanchez
    Member

    I doubt this will be possible for 1.0 without a lot of hacking. Pushing to the bottom of the backlog.

  9. afilp commented on Oct 13, 2015

    @afilp

    We experience the same problem on latest Chrome too. I wish that you find a possible fix as OpenStreet maps do not show well with these lines. Thanks.

  10. cmulders commented on Oct 23, 2015

    @cmulders
    Contributor

    Especially on darker maps or images (in my case) the issue is quite noticeable. I think that antialiasing in the browser causes the spaces between tiles due to the fractional transform during zooming, as mentioned by Ivan.

    Until a better solution is available, I will use this workaround (or hack) to make the tiles 1 pixel larger, with the side effect that they overlap by 1 px. Besides that the tiles will be slightly enlarged and scaled (by 1px).

    /* 
     * Workaround for 1px lines appearing in some browsers due to fractional transforms
     * and resulting anti-aliasing.
     * https://github.com/Leaflet/Leaflet/issues/3575
     */
    (function(){
        var originalInitTile = L.GridLayer.prototype._initTile
        L.GridLayer.include({
            _initTile: function (tile) {
                originalInitTile.call(this, tile);
    
                var tileSize = this.getTileSize();
    
                tile.style.width = tileSize.x + 1 + 'px';
                tile.style.height = tileSize.y + 1 + 'px';
            }
        });
    })()
  11. Eschon commented on Oct 23, 2015

    @Eschon
    ContributorAuthor

    @cmulders Thanks for the workaround!
    Since we mainly have maps of ski resorts that are mostly white and blue. We ignored the spaces for now, with your workaround it looks a lot better.

  12. hyperknot commented on Jan 14, 2016

    @hyperknot
    Collaborator

    Made a jsbin with cartodb dark tiles, the lines are visible even on retina screen:
    http://jsbin.com/ratoli/5/edit?html,js,output

    Safari / Chrome buggy, FF OK for me.

  13. 215 remaining items

  14. modified the milestones: 2.0, After 2.0 on Aug 7, 2024
  15. bsrinath4839 commented on Nov 5, 2024

    @bsrinath4839

    #3575 (comment)

    This one works with leaflet@1.7.1 But not with the latest leaflet@1.9.4

  16. bcwhite-code commented on Jan 4, 2025

    @bcwhite-code

    #3575 (comment) CSS worked for me with Leaflet 1.9.4. My tiles are 256px so I changed the example to match and the gridlines disappeared.

    Unfortunately, there was a small scaling issue where tiles touched, causing an occasional jagged edge. I tried setting the width and height to "256.1px" (instead of "257px") and that worked perfectly. The IDE warns that different browsers may handle this differently but I saw great results on both Chrome and Firefox.

  17. Falke-Design commented on Feb 28, 2025

    @Falke-Design
    Member

    It looks like chrome has finally implemented some fixes, let us hope ... https://issues.chromium.org/issues/40084005

  18. Falke-Design commented on Mar 1, 2025

    @Falke-Design
    Member

    Here is another solution which was found: openstreetmap/iD#10747

    #9560 (comment):

    I'm not sure if it's the best way, but I tested it by adding this SVG in the HTML

    <svg>
        <defs>
            <filter id="alpha-5">
                <feComponentTransfer>
                    <feFuncR type="identity"></feFuncR>
                    <feFuncG type="identity"></feFuncG>
                    <feFuncB type="identity"></feFuncB>
                    <feFuncA type="linear" slope="5"></feFuncA>
                </feComponentTransfer>
            </filter>
        </defs>
    </svg>
    

    and this CSS

    .leaflet-container img.leaflet-tile {
        filter: url(#alpha-5);
    }

    And I'm not sure why but in my testing it doesn't work as well as for openstreetmap-iD, only fixing the grid at 90% and 110%, but not completely at other zoom levels.

  19. seadog007 commented on Jun 7, 2025

    @seadog007

    The mix-blend-mode #8891 didn't work for me, it also break the workaround on my Mac.
    I am using Chrome v136.0.7103.114
    Image

    Image
  20. bcwhite-code commented on Jun 25, 2025

    @bcwhite-code

    Google "fixed" the issue on their end but only fixed one part of it, splitting the rest into a new issue. If you're encountering this, add a +1 to the new issue:

    https://issues.chromium.org/issues/401515597

  21. rkaravia commented on Oct 21, 2025

    @rkaravia
    Contributor

    Found yet another exotic issue with the mix-blend-mode: plus-lighter; workaround.

    With the workaround enabled, i.e. using standard Leaflet v1.9.4, print-to-pdf output in Firefox blows up the file size, and the map appears blurry on the pdf.

    Tested with Firefox 144 on Ubuntu.

    This is fixed by disabling the mix-blend-mode: plus-lighter; CSS rule.

    Steps

    Comparison

    Default output printing leafletjs.com to pdf

    Image

    leafletjs.com_Firefox_print_base.pdf (2.4 MB)

    Output with mix-blend-mode workaround disabled

    Image

    leafletjs.com_Firefox_print_disable-mix-blend-mode.pdf (706 kB)

  22. zdila commented on Oct 2, 2026

    @zdila

    One more case that mix-blend-mode: plus-lighter (1.9.4) doesn't cover: a 1-pixel dot where four tiles meet, at an integer zoom on a display with a fractional devicePixelRatio (1.75 here, Chrome and Vivaldi).

    Image

    It looks like sub-pixel tile positioning: with integer CSS positions, every tile lands at a fractional device pixel (e.g. 955 CSS px → 1671.25 device px), so all edges are anti-aliased. plus-lighter makes the two coverages along an edge add up to 1, but at a corner the four coverages seem to add up to slightly more than 1:

    • The dot is brighter than its surroundings and shows on mid tones only. On a flat #808080 layer it's obvious. On black, white or pure red/green/blue it's invisible, because 0 stays 0 and 255 clips.
    • All the dots disappear and reappear together while panning, as the fractional offset changes.

    So hillshading, which is mostly mid-grey, is the worst case.

    Workaround, deployed on freemap.sk: set a CSS translate of under one device pixel on the map pane, so that a resting tile's (scale(1) container) getBoundingClientRect() lands on whole device pixels. All tiles then sit on the device grid, and there's no partial coverage left to add up. It does nothing at an integer devicePixelRatio, or at fractional zoom, where tiles are scaled. Leaflet's coordinate math ignores the nudge, which is under a CSS pixel.

    function snapToDevicePixels(map) {
      const pane = map.getPane('mapPane');
    
      const snap = () => {
        const dpr = window.devicePixelRatio || 1;
        const tile = [...pane.querySelectorAll('.leaflet-tile-container')]
          .find((c) => c.style.transform.endsWith('scale(1)'))
          ?.querySelector('.leaflet-tile');
        if (Number.isInteger(dpr) || !tile) {
          pane.style.translate = '';
          return;
        }
        const [dx = 0, dy = 0] = pane.style.translate.split(' ').map((v) => parseFloat(v) || 0);
        const { left, top } = tile.getBoundingClientRect();
        const off = (v) => (Math.round(v * dpr) - v * dpr) / dpr;
        pane.style.translate = `${off(left - dx)}px ${off(top - dy)}px`;
      };
    
      // `layeradd` fires per marker too: coalesce to one layout read per frame.
      let frame = 0;
      const snapSoon = () => {
        frame ||= requestAnimationFrame(() => {
          frame = 0;
          snap();
        });
      };
    
      // Re-snap when the window moves to a screen with another pixel ratio.
      let dprQuery;
      const onDprChange = () => {
        watchDpr();
        snap();
      };
      const watchDpr = () => {
        dprQuery?.removeEventListener('change', onDprChange);
        dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);
        dprQuery.addEventListener('change', onDprChange);
      };
      watchDpr();
    
      map.on('move zoomend viewreset resize', snap);
      map.on('layeradd', snapSoon);
      snapSoon(); // layers added before this was attached
    }
  23. bcwhite-code commented on Oct 2, 2026

    @bcwhite-code

    https://issues.chromium.org/issues/401515597 for Chrome has seen some movement. It looks like it's been fixed but that fix was behind a flag (and probably an "experiment") to validate it before turning it on by default and closing the bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions