# How to Configure Custom XYZ or Vector Tile Basemaps in GeoLibre

> Learn to configure custom XYZ or vector tile basemaps in GeoLibre. Store a tile URL template in the global Zustand store to convert on-the-fly into MapLibre GL JS styles for raster or vector sources.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-01

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/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

```ts
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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:

```json
{
  "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:

```json
{
  "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`](https://github.com/opengeos/GeoLibre/blob/main/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

```ts
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:

```ts
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

```ts
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`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts).

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) | Zustand store with `basemapStyleUrl`, opacity, visibility |
| [`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts) | Style generation and MapLibre synchronization |
| [`apps/geolibre-desktop/src/lib/maptoolkit-basemap.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/maptoolkit-basemap.ts) | Detection helpers for hosted vs. custom URLs |
| [`packages/plugins/src/plugins/maplibre-basemap-control.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/maplibre-basemap-control.ts) | UI component for adding custom basemaps |
| [`apps/geolibre-desktop/src/lib/offline-basemaps.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/maptoolkit-basemap.ts) and [`map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/.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`](https://github.com/opengeos/GeoLibre/blob/main/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.