Documentation

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