Back to the blog

Customizing the map style client-side with Maputnik for MapLibre / Mapbox GL

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.

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:

  1. downloads the style.json file

  2. reads the sources to know where to load the tiles from (vector/raster)

  3. loads the sprites (icons) and glyphs (fonts)

  4. 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

(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:

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):

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:

  1. download style.json

  2. import it into Maputnik (Open / import menu)

  3. replace the sources with your own endpoints

  4. adjust layers, colors, icons, labels

  5. 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:

  1. declare the raster MBTiles in TileServer

  2. 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?)

Related articles

What our Explorer users say

⭐⭐⭐⭐⭐

I planned a 5-mile running loop around my neighborhood in minutes. The distance and pace estimate were spot on for my morning run before work.

Sarah K. New runner
⭐⭐⭐⭐⭐

With the isochrone mode, I plan my hikes in no time! I instantly see how far I can go with my available time. GPS tracking keeps me on trail in the Rockies.

Jessica W. Passionate hiker
⭐⭐⭐⭐⭐

We prepared a 25-mile cycling loop with friends and exported the GPX track. Everyone followed the same route on their Garmin without getting lost. Perfect organization!

Mike R. Weekend cyclist
⭐⭐⭐⭐⭐

Before a hike in Shenandoah, I checked the elevation and duration. The route followed real trails, which saved me from unpleasant surprises. Very reliable!

Emily L. Beginner hiker
⭐⭐⭐⭐⭐

I wanted an easy walk for my kids. I set a short max distance and MapPlanner helped me create the perfect loop. We had a wonderful family afternoon.

David N. Family outing
⭐⭐⭐⭐⭐

I love planning my outings in advance. I saved several routes for the weekends. It saves me so much time compared to figuring it out on the go.

Karen G. Occasional hiker
⭐⭐⭐⭐⭐

By exploring the routes around my area, I discovered great trails I didn’t know about. MapPlanner opened new places for me to explore right in my backyard.

Chris P. Trail discovery
⭐⭐⭐⭐⭐

I keep track of my progress with the history feature. Comparing my running pace and distance over the months keeps me motivated and accountable.

Ashley T. Performance tracking
⭐⭐⭐⭐⭐

For our MTB group ride, we created the route and sent the GPX to everyone. Nobody got separated — the whole group stayed together from start to finish.

Brian B. Club activity
⭐⭐⭐⭐⭐

I love taking photos while hiking. MapPlanner automatically places my pictures on the map. It’s awesome to review the route and share it with friends.

Lauren F. Discovery outing
⭐⭐⭐⭐⭐

Depending on the activity, I switch the map style: topo for the mountains, street for the city, satellite to scout terrain. It makes route planning super intuitive.

Tyler S. Experienced hiker
⭐⭐⭐⭐⭐

MapPlanner now estimates my times based on my own pace. I know exactly how long my next run will take. Perfect for squeezing in a workout on a tight schedule.

Rachel J. Aspiring runner

Popular routes

Discover the most popular routes near you

Miles at the Meadow - 5k / 10K (2 loops)
pete.augrom
Running
Very easy

Miles at the Meadow - 5k / 10K (2 loops)

3.095mi
•
33m
•
5.63mph
Virginia - The Plains
Fusion Invitational High School 5K Course
nm8zkrv8hk
Running
Easy

Fusion Invitational High School 5K Course

3.201mi
•
34m
•
5.65mph
Colorado - Commerce City
ada
rajan
Running
Strenuous

ada

11.669mi
•
4h10m
•
2.8mph
New York - New York
Route 2 - Prayer Walk Connecting Churches (Ferguson)
SR St. Louis
Walk
Very easy

Route 2 - Prayer Walk Connecting Churches (Ferguson)

1.441mi
•
31m
Missouri - Ferguson
2026 HHHH 25mi Route
buschlive
Bike
Moderate

2026 HHHH 25mi Route

24.79mi
•
2h30m
•
9.92mph
Texas - Hutchins
Central park
Fibula
Running
Moderate

Central park

6.393mi
•
1h
•
6.39mph
New York - New York
Miles at the Meadow - 1 mile
pete.augrom
Running
Very easy

Miles at the Meadow - 1 mile

1.012mi
•
11m
•
5.52mph
Virginia - The Plains
Chincoteague National Wildlife Refuge
Gaynor
Bike
Very easy

Chincoteague National Wildlife Refuge

9.532mi
•
2h
•
4.77mph
Virginia - Chincoteague Island