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 stringbasemapOpacity— 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:
- Detection — checks if
basemapStyleUrlis a Mapbox style JSON, Maptoolkit-hosted URL, or raw template - Resolution — for raw XYZ/vector URLs, calls helpers to generate a style JSON
- 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
- Click "Add Data" in the left panel
- Select "Basemap"
- Choose "Custom XYZ"
- 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
- In "Add Data → Basemap", select "Custom Vector Tiles"
- 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-layernames match the actual layer names in the PBF - Inspect with
map.querySourceFeatures('basemap')to confirm data loads
Opacity changes don't apply
- Ensure
basemapVisibleistrue; opacity on hidden layers has no effect
Summary
- Store-driven architecture: Custom basemaps live in
basemapStyleUrl(global Zustand store) —MapControllerhandles 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-layerreferences - No core modifications: Detection logic in
maptoolkit-basemap.tsandmap-controller.tshandles all URL patterns - Programmatic access: Import
useAppStoreand callsetBasemapStyleUrl()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →