On a Mapbox map, how to keep a layer always visible, no matter the zoom level?

I have tried to understand the underlying mechanisms of the data display processing at the tile level but I have not found any literature explicit enough for me to understand how it all works.

I have a simple need though: to keep a layer of symbols always visible.

I have tested these attributes for a long time: 'icon-allow-overlap': true, icon-ignore-placement': true, text-allow-overlap': true, text-ignore-placement': true I also tried to play with the layers and sources orde, the visibility by zoom level, ... But I have the impression that an internal workings overload all this and take over my statements.

I guess it's something to do with the data loaded by tiles... but I can't go further to understand the "problem".

I'm interested in any explanation or link (or examples!) that would help me understand the workings of Mapbox in this regard.


  • If your data is in a vector tile source, the data availability in the tiles limits your ability to control which zoom levels the symbols would be visible. The main reason being that when you zoom outside the zoom range the data is available within the vector tile layer, the data becomes unavailable. If you control the vector tile source, you could make the data available in all zoom levels of the vector tiles, but note that this may create an issue if you have a massive number of points when zoomed all the way out.

    If the data is loaded through a GeoJSON source, then you can make a symbol always appear across all zoom levels by using the four icon/text options you mentioned.

    If you are using vector tiles and have no control over the tiles, you could potentially make a hack that captures the data from the vector tiles and store the data in a geojson source. You would load the vector tiles using a hiden layer (make things transparent or not visible so the tiles still get requested), then as the map moves, retrieve all the geometries in the viewable map area from that source. Ideally you would have a unique identifier that you could use to keep track of geometries so you don't store/capture the same geometry more than once. Note that if the geometries are captured from the vector tiles when zoomed out, the accuracy of the positions may be low since the coordinates would have been snapped to pixels at that zoom level, so you could potentially keep track of the zoom level a geometry was captured at, and if you encounter the same geometry again later when zoomed in more you could replace the geometries coordinates accordingly to improve its accuracy. This would require a decent amount of code to get working correctly, but if you have no other option, this should be viable.


    Going through your live sample I managed to reproduce the issue. The console had some errors indicating that the images for the icons hadn't been loaded. Looking at your code you do have code to load icons but it is asynchronous and so every once and a while the icons haven't finished loading before the code for the layers is processed, thus causing these errors. To fix this you would need to wait for the icons to finish loading before creating the layer. For example:

    import "./styles.css";
    import datas from "./datas.json";
    import path from "./path.json";
    import chateaux_agrements from "./pins/chateaux_agrements-c.png";
    import chateaux_ducaux from "./pins/chateaux_ducaux-c.png";
    import chateaux_rohan from "./pins/chateaux_rohan-c.png";
    import mottes_castrales from "./pins/mottes_castrales-c.png";
    mapboxgl.accessToken =
    const map = new mapboxgl.Map({
      container: "map",
      style: "mapbox://styles/cladjidane/ckh0nxrpm03fr19lkkmp02yfz",
      center: [-2.842, 47.765],
      zoom: 8
    const images = [
      { url: chateaux_agrements, id: "chateaux_agrements" },
      { url: chateaux_ducaux, id: "chateaux_ducaux" },
      { url: chateaux_rohan, id: "chateaux_rohan" },
      { url: mottes_castrales, id: "mottes_castrales" }
    map.on("load", function () {
      map.addSource("game_source", {
        type: "geojson",
        data: {
          type: "FeatureCollection",
          features: datas.features
      map.addSource("path_source", {
        type: "geojson",
        data: {
          type: "FeatureCollection",
          features: path.features
      //Keep track of how many images have loaded.
      var imagesLoaded = 0;
  => {
        map.loadImage(img.url, (error, image) => {
          if (error) throw error;
          map.addImage(, image);
          //Increment the number of images that have been loaded.
          //Check to see if all images have been loaded.
          if(imagesLoaded === images.length) {
            //Run code to add the layers.
        return true;
      function addLayers() {
          id: "path_line",
          source: "path_source",
          type: "line",
          layout: {
            "line-join": "round",
            "line-cap": "round"
          paint: {
            "line-color": "white",
            "line-dasharray": [1, 3],
            "line-width": [
          id: "game_step",
          source: "game_source",
          type: "circle",
          filter: ["==", "type", "step"],
          paint: {
            "circle-radius": 6,
            "circle-color": "rgba(255,255,255, .8)"
          id: "game_castel",
          source: "game_source",
          type: "symbol",
          filter: ["all", ["==", "type", "castel"]],
          layout: {
            "text-variable-anchor": ["top", "bottom", "left", "right"],
            "text-size": 6,
            "text-offset": [0, 2.5],
            "icon-image": [
              ["get", "castelType"],
              ["Châteaux d'agrément"],
              ["Châteaux des Rohan"],
              ["Châteaux ducaux"],
              ["Mottes castrales"],
            "icon-size": 0.35,
            "icon-allow-overlap": true,
            "icon-ignore-placement": true,
            "text-allow-overlap": true,
            "text-ignore-placement": true,
            "text-field": ["get", "name"]
          paint: {
            "text-color": "red",
            "text-halo-color": "#fff",
            "text-halo-width": 2