Integrations

MapLibre GL JS

Integrate tileserver-rs with MapLibre GL JS for interactive web maps

MapLibre GL JS is the most popular open-source library for rendering vector maps in the browser. This guide shows you how to use tileserver-rs as your tile source.

Basic Setup

Using a Style JSON

If you've configured styles in tileserver-rs, you can use them directly:

<!DOCTYPE html>
<html>
<head>
  <title>MapLibre + tileserver-rs</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.css" rel="stylesheet" />
  <script src="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.js"></script>
  <style>
    body { margin: 0; }
    #map { height: 100vh; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script>
    const map = new maplibregl.Map({
      container: 'map',
      style: 'http://localhost:8080/styles/your-style/style.json',
      center: [0, 0],
      zoom: 2
    });
  </script>
</body>
</html>

Using TileJSON (Vector Tiles Only)

If you only have vector tile sources without a style, reference the TileJSON and define your own layers:

const map = new maplibregl.Map({
  container: 'map',
  style: {
    version: 8,
    sources: {
      'tiles': {
        type: 'vector',
        url: 'http://localhost:8080/data/your-source.json'
      }
    },
    layers: [
      {
        id: 'background',
        type: 'background',
        paint: { 'background-color': '#f8f4f0' }
      },
      {
        id: 'water',
        type: 'fill',
        source: 'tiles',
        'source-layer': 'water',
        paint: { 'fill-color': '#a0cfdf' }
      },
      {
        id: 'roads',
        type: 'line',
        source: 'tiles',
        'source-layer': 'transportation',
        paint: {
          'line-color': '#888',
          'line-width': ['interpolate', ['linear'], ['zoom'], 10, 0.5, 18, 4]
        }
      }
    ]
  },
  center: [11.255, 43.77],
  zoom: 12
});

Using Raster Tiles

tileserver-rs can render vector tiles to raster images on the server. This is useful for:

  • Clients that don't support vector tiles (older browsers, some native apps)
  • Print/export scenarios
  • Reducing client-side rendering load
const map = new maplibregl.Map({
  container: 'map',
  style: {
    version: 8,
    sources: {
      'raster-tiles': {
        type: 'raster',
        tiles: ['http://localhost:8080/styles/your-style/{z}/{x}/{y}.png'],
        tileSize: 512
      }
    },
    layers: [
      {
        id: 'raster-layer',
        type: 'raster',
        source: 'raster-tiles'
      }
    ]
  },
  center: [0, 0],
  zoom: 2
});

Retina/HiDPI Tiles

For sharper maps on high-DPI displays:

const pixelRatio = window.devicePixelRatio || 1;
const scale = pixelRatio > 1 ? '@2x' : '';

const map = new maplibregl.Map({
  container: 'map',
  style: {
    version: 8,
    sources: {
      'raster-tiles': {
        type: 'raster',
        tiles: [`http://localhost:8080/styles/your-style/{z}/{x}/{y}${scale}.png`],
        tileSize: 512
      }
    },
    layers: [
      { id: 'raster-layer', type: 'raster', source: 'raster-tiles' }
    ]
  }
});

Framework Integration

React

import { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

export function Map({ styleUrl, center, zoom }) {
  const mapContainer = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    if (map.current) return; // Already initialized

    map.current = new maplibregl.Map({
      container: mapContainer.current,
      style: styleUrl,
      center: center || [0, 0],
      zoom: zoom || 2
    });

    return () => map.current?.remove();
  }, [styleUrl, center, zoom]);

  return <div ref={mapContainer} style={{ height: '100%' }} />;
}

// Usage
<Map
  styleUrl="http://localhost:8080/styles/bright/style.json"
  center={[11.255, 43.77]}
  zoom={12}
/>

Vue 3

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const props = defineProps({
  styleUrl: { type: String, required: true },
  center: { type: Array, default: () => [0, 0] },
  zoom: { type: Number, default: 2 }
});

const mapContainer = ref(null);
let map = null;

onMounted(() => {
  map = new maplibregl.Map({
    container: mapContainer.value,
    style: props.styleUrl,
    center: props.center,
    zoom: props.zoom
  });
});

onUnmounted(() => {
  map?.remove();
});
</script>

<template>
  <div ref="mapContainer" class="h-full w-full" />
</template>

Svelte

<script>
  import { onMount, onDestroy } from 'svelte';
  import maplibregl from 'maplibre-gl';
  import 'maplibre-gl/dist/maplibre-gl.css';

  export let styleUrl;
  export let center = [0, 0];
  export let zoom = 2;

  let container;
  let map;

  onMount(() => {
    map = new maplibregl.Map({
      container,
      style: styleUrl,
      center,
      zoom
    });
  });

  onDestroy(() => {
    map?.remove();
  });
</script>

<div bind:this={container} class="h-full w-full" />

Adding Controls

const map = new maplibregl.Map({
  container: 'map',
  style: 'http://localhost:8080/styles/bright/style.json',
  center: [11.255, 43.77],
  zoom: 12
});

// Navigation controls (zoom, rotate)
map.addControl(new maplibregl.NavigationControl());

// Scale bar
map.addControl(new maplibregl.ScaleControl({
  maxWidth: 100,
  unit: 'metric'
}));

// Fullscreen button
map.addControl(new maplibregl.FullscreenControl());

// Geolocation
map.addControl(new maplibregl.GeolocateControl({
  positionOptions: { enableHighAccuracy: true },
  trackUserLocation: true
}));

Adding Markers and Popups

// Add a marker
const marker = new maplibregl.Marker({ color: '#ff0000' })
  .setLngLat([11.255, 43.77])
  .setPopup(new maplibregl.Popup().setHTML('<h3>Florence</h3><p>Birthplace of the Renaissance</p>'))
  .addTo(map);

// Add a popup on click
map.on('click', 'poi-layer', (e) => {
  const feature = e.features[0];
  new maplibregl.Popup()
    .setLngLat(feature.geometry.coordinates)
    .setHTML(`<strong>${feature.properties.name}</strong>`)
    .addTo(map);
});

GeoJSON Overlays

Load GeoJSON from tileserver-rs static files endpoint:

map.on('load', () => {
  // Add GeoJSON source
  map.addSource('routes', {
    type: 'geojson',
    data: 'http://localhost:8080/files/routes.geojson'
  });

  // Add line layer
  map.addLayer({
    id: 'route-line',
    type: 'line',
    source: 'routes',
    paint: {
      'line-color': '#ff0000',
      'line-width': 3
    }
  });
});

Performance Tips

1. Use Vector Tiles When Possible

Vector tiles are smaller and allow client-side styling:

// Prefer this (vector)
{ type: 'vector', url: '/data/source.json' }

// Over this (raster) - unless you need server-side rendering
{ type: 'raster', tiles: ['/styles/style/{z}/{x}/{y}.png'] }

2. Limit Tile Requests

Set appropriate min/max zoom to avoid unnecessary requests:

{
  type: 'vector',
  url: '/data/source.json',
  minzoom: 0,
  maxzoom: 14  // Don't request beyond z14
}

3. Use Hash for Deep Linking

Enable URL hash to preserve map state:

const map = new maplibregl.Map({
  container: 'map',
  style: 'http://localhost:8080/styles/bright/style.json',
  hash: true  // URL updates with #zoom/lat/lng
});

Troubleshooting

CORS Errors

If you see CORS errors, ensure tileserver-rs has your domain in cors_origins:

[server]
cors_origins = ["http://localhost:3000", "https://yourdomain.com"]

Tiles Not Loading

Check the browser Network tab for failed requests. Common issues:

  • Wrong source ID in URL
  • Tiles outside available zoom range
  • Server not running

Style Validation Errors

Use MapLibre's style validator:

const errors = maplibregl.validateStyle(styleJson);
if (errors.length) {
  console.error('Style errors:', errors);
}

Next Steps