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.
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>
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
});
tileserver-rs can render vector tiles to raster images on the server. This is useful for:
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
});
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' }
]
}
});
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}
/>
<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>
<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" />
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
}));
// 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);
});
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
}
});
});
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'] }
Set appropriate min/max zoom to avoid unnecessary requests:
{
type: 'vector',
url: '/data/source.json',
minzoom: 0,
maxzoom: 14 // Don't request beyond z14
}
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
});
If you see CORS errors, ensure tileserver-rs has your domain in cors_origins:
[server]
cors_origins = ["http://localhost:3000", "https://yourdomain.com"]
Check the browser Network tab for failed requests. Common issues:
Use MapLibre's style validator:
const errors = maplibregl.validateStyle(styleJson);
if (errors.length) {
console.error('Style errors:', errors);
}