Once the tiles (MBTiles/PMTiles) are ready and served by a server (TileServer GL, Martin, etc.), the “map” users see is mostly defined by a style file.
The style describes what to display (roads, paths, POIs…), when to display it (zoom levels) and how to display it (colors, widths, labels, icons).
This article walks through a complete method to create, edit, export and deploy a MapLibre/Mapbox GL style, and to handle fonts, glyphs and sprites.
Style specification
The style format is the Mapbox GL / MapLibre one.
-
Official specification: https://maplibre.org/maplibre-style-spec/
The style is a JSON document that contains, among other things:
-
sources (vector/raster): where the data comes from
-
layers: how to render each “layer” (roads, buildings, water…)
-
sprites: icons (POIs)
-
glyphs: fonts (labels)
Example styles (inspiration)
Some “reference” styles worth comparing rendering choices against:
-
Maputnik
-
Mapbox Outdoors
-
Mapbox Streets
-
MapTiler Outdoor
-
Komoot
-
Outdooractive
-
SityTrail
-
Openrunner
-
France Vélo Tourisme
-
Strava
-
AllTrails
-
Helloways
-
onthegomap
-
FranceTopo
1) Understanding the role of the style in client-side rendering
In the browser (MapLibre GL / Mapbox GL), the engine:
-
downloads the style.json file
-
reads the sources to know where to load the tiles from (vector/raster)
-
loads the sprites (icons) and glyphs (fonts)
-
applies the style’s layers to draw the map at the right zoom
Important: vector tiles hold data, not rendering.
The style is therefore where you control:
-
information density (what, at which zoom)
-
hierarchy (drawing order)
-
readability (colors, text halo, widths)
-
performance (number of layers, filters, complexity)
Plan your own route
MapPlanner helps you calculate distances, elevation, duration and plan your hikes, runs and bike rides on an interactive map.
- Automatic distance & elevation calculation
- Topographic, satellite maps
- GPX/KML export for your GPS
- Real-time GPS tracking
2) An important limitation: one style, several MBTiles
A single style cannot be shared by several mbtiles sources, so you have to either:
-
use a single mbtiles (the whole world)
-
merge the mbtiles if they do not overlap (overlapping files are not supported)
-
duplicate the style for each source
Why?
Because a style references its sources by identifier (sources.{name}), and “mbtiles://…” servers or TileJSON endpoints are usually built around one dataset. If you switch datasets (or add several), you have to:
-
either put everything into a single source,
-
or declare several sources (when supported),
-
or derive one style per source.
In practice, the simplest and most robust strategy is often a single global dataset (or one main dataset) with styles adapted to it.
3) Prerequisites: fonts, glyphs and sprites
To display correctly:
-
labels (city names, roads…) → you need fonts + glyphs
-
icons (POIs) → you need sprites
Fonts
Download the fonts:
git clone https://github.com/klokantech/klokantech-gl-fonts
Then:
Remove every KlokanTech prefix from the directory names.
Goal: clean fontstack names that match the ones used by the style (e.g. Noto Sans Regular).
Sprites / glyphs from MapTiler
To get started quickly, you can fetch sprites/fonts from MapTiler:
Then you can:
-
pick an existing style,
-
grab its sprites and fonts,
-
host them on your own server to be self-sufficient.
4) Creating your own icons / sprites
If you want custom POIs (e.g. long-distance trails, viewpoints, drinking water, parking…), you will build your sprites from SVG files.
OSM tag reference
-
Every type declared in OSM: Map features
(The key point: your icons must match categories that actually exist in your OSM data / your layers.)
Tool: osm-liberty (Maputnik)
A handy repository to build a sprite from SVG files:
Get the sources:
git clone https://github.com/maputnik/osm-liberty
cd osm-liberty/
npm install
Finding SVG icons
Icon sources:
-
icons made by Mapbox: https://labs.mapbox.com/maki-icons/
Size constraints
Create each icon in 2 sizes:
-
21x21px
-
17x17px
Why two sizes? Because styles often use different sizes depending on zoom / density / importance. And for sharpness, “retina” sprites are handled too.
Generating the sprites (normal + retina)
Install the tool:
npm install @beyondtracks/spritezero-cli
Important:
The svgs directory must contain only SVG files and no sub-directory (by default, move the SVG files from svgs_iconset up to the parent directory and delete the other directories).
Generation:
./node_modules/@beyondtracks/spritezero-cli/bin/spritezero sprites/sprite ./svgs/
./node_modules/@beyondtracks/spritezero-cli/bin/spritezero --retina sprites/sprite@2x ./svgs/
Compression (recommended for network performance):
-
TinyPNG: https://tinypng.com/
Upload to the server:
scp -r ./sprites/* user@your-server:/home/www/tileserver/sprites
5) Creating / editing the style with Maputnik
Maputnik is a visual editor for Mapbox/MapLibre styles.
Running Maputnik with Docker
mkdir maputnik
sudo docker pull maputnik/editor
sudo docker run -it --rm -p 8888:8888 maputnik/editor
Alternative: the latest version from the MapLibre repository
git clone https://github.com/maplibre/maputnik.git
cd maputnik/
npm install
Then start it:
npm run start -- --host 0.0.0.0
Starting from an existing style (recommended)
To go faster, start from a predefined style:
Typical workflow:
-
download style.json
-
import it into Maputnik (Open / import menu)
-
replace the sources with your own endpoints
-
adjust layers, colors, icons, labels
-
export the final style
Connecting Maputnik to your data (TileJSON)
In Maputnik:
-
open “Data source”
-
replace the URL with the TileJSON URL copied from your TileServer-GL server:
http://your-server:5010/data/tileset.json
Why TileJSON?
Because Maputnik (and many tools) can consume a source through an endpoint that describes how to fetch the tiles.
Then:
-
edit visually
-
export a new style as .json
6) Using an existing (ready-made) style
If you already have a style (e.g. a “complete” Maputnik export), you can:
-
import the complete style (maputnik.json) from the Open menu
If needed, configure each part:
1) Data sources
Edit the JSON sources in Data sources (with the full URL):
#map-planner : /data/world.json
#hillshade : /data/hillshade_12.json
2) Style Settings
Point Style Settings to your own assets (with the full URL):
Sprite URL : /styles/map-planner/sprite
Glyphs URL : /fonts/{fontstack}/{range}.pbf
Then reload the page.
7) Exporting and adapting the style to your server
Once exported, the style must match your runtime (server + asset paths).
Example adjustment (sources + sprite + glyphs):
"sources": {
"map-planner": {
"type": "vector",
"url": "mbtiles://europe.mbtiles"
}
},
"sprite": "map-planner/sprite",
"glyphs": "{fontstack}/{range}.pbf",
8) Adding a raster basemap for low zooms (0 → 6)
At “world” zooms (0 to 6), drawing all the vector data can be:
-
useless (too much detail)
-
heavy (many objects to draw)
-
less readable
Solution: display a light raster background (PNG) at low zooms.
Download the raster style you want:
Then:
-
declare the raster MBTiles in TileServer
-
add it as a Raster TileJSON URL in Maputnik
Result: a “clean” rendering of the whole world (low zoom), then a gradual switch to the detailed vector layers as you zoom in.
9) Useful tools to build a consistent style
Filtering / understanding OSM objects
To see which objects exist in an area and which tags to use in your style filters:
OSM tag reference
This is the foundation that links OSM data → tilemaker/planetiler layers → style layers.
10) Customization best practices
A few rules that save a lot of frustration:
-
Start from an existing style (OpenMapTiles), then iterate.
-
Keep stable naming between:
-
source names (map-planner, hillshade, etc.)
-
layer names in the tiles (the ones produced by tilemaker/planetiler)
-
sprite names (SVG files → sprite.json)
-
-
Play with minzoom/maxzoom in the layers: it is the key to readability and performance.
-
Test as you go:
-
tile loading (source OK?)
-
glyphs (labels visible?)
-
sprites (icons showing?)
-