How to Configure Custom XYZ or Vector Tile Basemaps in GeoLibre

Set a custom basemap by storing a tile URL template in GeoLibre's global Zustand store (basemapStyleUrl), which MapController converts on-the-fly into a MapLibre GL JS style with raster or vector sources.

GeoLibre, the open-source geospatial workspace built on MapLibre GL JS, lets you add custom basemaps without modifying core code. Whether you need satellite imagery from an XYZ service or custom vector tiles, the configuration flows through a centralized state system that generates compatible styles dynamically. This guide covers both UI-based and programmatic methods using the actual source code structure from the opengeos/GeoLibre repository.

How Basemap Configuration Works Under the Hood

GeoLibre's basemap system separates storage, detection, and rendering into three distinct layers. Understanding this flow helps troubleshoot issues and extend functionality.

The Global Store Holds the Source URL

In packages/core/src/store.ts, the Zustand store defines three key fields:

  • basemapStyleUrl — the URL template or style JSON string
  • basemapOpacity — layer opacity (0-1)
  • basemapVisible — boolean visibility toggle
// Key lines from packages/core/src/store.ts
basemapStyleUrl: string;
setBasemapStyleUrl: (url: string) => void;

When you add a custom basemap through the UI or API, only this string changes. The actual MapLibre style generation happens elsewhere.

MapController Builds the Style Dynamically

The MapController class in packages/map/src/map-controller.ts reacts to store changes. It runs detection logic then constructs minimal, valid MapLibre styles:

  1. Detection — checks if basemapStyleUrl is a Mapbox style JSON, Maptoolkit-hosted URL, or raw template
  2. Resolution — for raw XYZ/vector URLs, calls helpers to generate a style JSON
  3. Application — passes the result to MapLibre GL JS via applyStyleToMap

This architecture means custom basemaps require zero core code changes — just valid URL patterns.

Adding a Custom XYZ Raster Basemap

XYZ raster tiles are the most common custom basemap type. GeoLibre supports standard {z}, {x}, {y} templating with optional {s} subdomains.

UI Method: Add Data → Basemap

  1. Click "Add Data" in the left panel
  2. Select "Basemap"
  3. Choose "Custom XYZ"
  4. Enter your tile URL template

https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png

When confirmed, GeoLibre stores this string and the next render cycle produces:

{
  "version": 8,
  "sources": {
    "basemap": {
      "type": "raster",
      "tiles": ["https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"],
      "tileSize": 256
    }
  },
  "layers": [
    {
      "id": "basemap",
      "type": "raster",
      "source": "basemap"
    }
  ]
}

The Opacity and Visibility controls in the UI directly modify basemapOpacity and basemapVisible in the store. MapController syncs these to the layer's paint properties automatically.

Supported URL Patterns

Pattern Example Notes
Standard XYZ https://server.com/{z}/{x}/{y}.png Most common
Subdomain https://{s}.server.com/... Rotates through a-d
Retina/HiDPI https://server.com/{z}/{x}/{y}@2x.png Set tileSize: 512 if needed

Adding a Custom Vector Tile Basemap

Vector tiles require slightly different handling since they need a source-layer reference for styling.

UI Method: Custom Vector Tiles Option

  1. In "Add Data → Basemap", select "Custom Vector Tiles"
  2. Provide a PBF-serving tile template:

https://tiles.example.com/vt/{z}/{x}/{y}.pbf

GeoLibre generates a vector source with a default fill layer:

{
  "version": 8,
  "sources": {
    "basemap": {
      "type": "vector",
      "tiles": ["https://tiles.example.com/vt/{z}/{x}/{y}.pbf"],
      "maxzoom": 14
    }
  },
  "layers": [
    {
      "id": "basemap",
      "type": "fill",
      "source": "basemap",
      "source-layer": "land",
      "paint": { "fill-color": "#f0f0f0" }
    }
  ]
}

Using Existing Style JSON

If your vector tile service provides a complete MapLibre style (e.g., Mapbox, MapTiler), paste that URL directly. The isMapboxStyleUrl helper in the detection logic recognizes these and treats them as remote styles rather than generating a minimal style.

Per the Maptoolkit integration in apps/geolibre-desktop/src/lib/maptoolkit-basemap.ts, services like https://tiles.openfreemap.org/ get special handling for raster conversions, but standard vector styles pass through unchanged.

Programmatic Configuration

For plugins, automated workflows, or testing, interact with the store directly.

Set a Simple XYZ Basemap

import { useAppStore } from "@geolibre/core";

function setCustomXYZBasemap(urlTemplate: string): void {
  useAppStore.getState().setBasemapStyleUrl(urlTemplate);
}

// Usage
setCustomXYZBasemap("https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}");

Provide a Full Vector Style JSON

For vector tiles requiring custom layers, pass a stringified style object:

const styleJson = {
  version: 8,
  sources: {
    basemap: {
      type: "vector",
      tiles: ["https://mytiles.org/vt/{z}/{x}/{y}.pbf"],
      maxzoom: 14,
      attribution: "© MyCustomTiles"
    }
  },
  layers: [
    {
      id: "water",
      type: "fill",
      source: "basemap",
      "source-layer": "water",
      paint: { "fill-color": "#a0c8f0" }
    },
    {
      id: "roads",
      type: "line",
      source: "basemap",
      "source-layer": "roads",
      paint: { "line-color": "#ffffff", "line-width": 1 }
    }
  ]
};

useAppStore.getState().setBasemapStyleUrl(JSON.stringify(styleJson));

The MapController detects JSON vs. plain URL by attempting to parse the string.

Controlling Opacity and Visibility

const store = useAppStore.getState();

// Set 70% opacity
store.setBasemapOpacity(0.7);

// Hide basemap
store.setBasemapVisible(false);

These calls trigger immediate updates to the MapLibre map instance via reactive subscriptions in packages/map/src/map-controller.ts.

Key Implementation Files

File Purpose
packages/core/src/store.ts Zustand store with basemapStyleUrl, opacity, visibility
packages/map/src/map-controller.ts Style generation and MapLibre synchronization
apps/geolibre-desktop/src/lib/maptoolkit-basemap.ts Detection helpers for hosted vs. custom URLs
packages/plugins/src/plugins/maplibre-basemap-control.ts UI component for adding custom basemaps
apps/geolibre-desktop/src/lib/offline-basemaps.ts Sentinel-based offline basemap registry

Common Issues and Solutions

Basemap doesn't appear

  • Verify {z}, {x}, {y} placeholders are lowercase and braced
  • Check CORS headers on the tile server
  • Confirm the URL returns valid images/PBFs at zoom 0-2

Vector tiles show blank map

  • Verify source-layer names match the actual layer names in the PBF
  • Inspect with map.querySourceFeatures('basemap') to confirm data loads

Opacity changes don't apply

  • Ensure basemapVisible is true; opacity on hidden layers has no effect

Summary

  • Store-driven architecture: Custom basemaps live in basemapStyleUrl (global Zustand store) — MapController handles MapLibre conversion
  • XYZ raster: Provide template with {z}/{x}/{y}, optionally {s} for subdomains
  • Vector tiles: Use PBF-serving template or full style JSON; requires valid source-layer references
  • No core modifications: Detection logic in maptoolkit-basemap.ts and map-controller.ts handles all URL patterns
  • Programmatic access: Import useAppStore and call setBasemapStyleUrl() with URL or JSON string
  • Synced controls: Opacity and visibility are reactive store fields that automatically update the rendered map

Frequently Asked Questions

What URL template format does GeoLibre require for XYZ tiles?

GeoLibre requires standard slippy map template placeholders: {z} for zoom level, {x} for tile column, and {y} for tile row. Optional {s} enables subdomain rotation across servers. The template must be a complete URL that returns raster images (typically PNG or JPEG). Invalid templates fail silently at the resolveMapStyle stage — check browser network requests to verify tile loading.

Can I use a Mapbox Studio style URL directly?

Yes. The isMapboxStyleUrl check in the style resolution pipeline recognizes mapbox:// and https://api.mapbox.com/styles/v1/ URLs. These pass through as remote styles rather than generating a minimal wrapper. For styles requiring an access token, append it as a query parameter or configure it in your MapLibre transformRequest handler.

How do I set a custom basemap when opening a project programmatically?

Before initializing the map or loading a .geolibre.json project file, import useAppStore and call setBasemapStyleUrl with your template. The MapController reads this value during its initialization sequence in packages/map/src/map-controller.ts. If setting after project load, the reactive store subscription triggers an immediate style update without reloading the project.

Why does my vector basemap show only a gray background?

Gray backgrounds indicate the vector source loaded successfully but layer styling failed. Common causes: mismatched source-layer names (verify with vtquery or inspection tools), missing required paint properties, or zoom level limits in the source. GeoLibre's default generated style uses "source-layer": "land" — if your tiles use different layer names, provide a complete style JSON with correct references.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →