AI-generated: These articles are Claude Opus 4.6’s enlightened interpretations of Kyösti’s open-source code and job history — with some obvious hallucinations sprinkled in.

Custom Map Rendering with MapLibre GL: Beyond the Defaults

MapLibre GL is the open-source successor to Mapbox GL JS, and it's excellent. But every MapLibre map I see in production looks like a navigation app. The library's defaults — road widths, label sizes, color stops — are tuned for getting directions, not for exploring terrain or conveying spatial data. Here's how to build a map that looks intentional.

The Style Spec: A Brief Orientation

MapLibre GL styles are JSON documents. A style file has a few top-level keys: sources (where the data comes from), layers (how to render each piece of data), glyphs (font rendering), sprite (icon images), and a handful of metadata properties. Everything that makes your map look like something happens in the layers array.

Each layer has a type (fill, line, symbol, circle, raster, hillshade, fill-extrusion), a source reference, a source-layer if the source is vector tiles, an optional filter, and paint and layout properties. Paint controls appearance (colors, widths, opacity). Layout controls behaviour (visibility, label placement, text fields).

The thing that makes MapLibre styles powerful — and the thing most tutorials barely touch — is the expression language. MapLibre supports expressions in almost every property value, which means you can write logic like "the width of this road changes based on the zoom level and the road's class attribute, scaled with an exponential curve." This is not optional for a professional-looking map; it's the mechanism by which map features read as spatially correct at different scales.

Start With Typography

Map labels are the first thing users consciously judge. Before you've looked at a single road line or polygon fill, the labels tell you whether this is a careful product or a demo. There are three decisions that matter most:

Font stack selection

MapLibre GL renders text from SDF (Signed Distance Field) glyph sets, not system fonts. You need to host or reference glyph tiles that encode the fonts you want. The standard approach is to use the Maptiler font CDN or host your own glyph tiles generated with fontnik. Don't use the Mapbox-hosted glyphs — they're not licensed for use outside Mapbox products.

For cartographic use, I favour humanist sans-serif faces for place names (Noto Sans works well and has comprehensive Unicode coverage for Finnish characters including the ä, ö, and å that appear throughout Nordic place names) and a slightly heavier weight for POI labels that need to read over complex backgrounds.

Text halo for legibility

The text-halo-color and text-halo-width paint properties are your primary tool for making labels readable over varied backgrounds — road labels over forest, lake names over water polygons. A white halo at 1.5px width makes text readable on almost any light background; a semi-transparent white halo preserves the background texture while maintaining legibility. Skipping the halo is the single most reliable way to make a map look amateurish.

Collision rules

MapLibre's collision detection determines which labels to show when they overlap. The default behaviour is generally correct, but you can tune it: symbol-sort-key lets you prioritise which features win when two labels compete for the same space. For topographic maps, I sort by feature importance (major cities win over minor settlements win over individual POIs) so that the visible label set represents the most significant features at each zoom level.

Color Philosophy for Cartographic Use

The default basemap colour palette you'll find in most MapLibre styles is tuned for navigation: grey roads, blue water, white background, orange highlights for your route. This works for Google Maps clone aesthetics and for nothing else.

Cartographic colour is a domain in itself. Three approaches:

Reference basemaps (topographic use) aim for naturalistic representation: forests are green, water is blue, open land is near-white or pale yellow, urban areas are light grey or warm tan. The palette is muted and earthy because strong saturation competes with the data overlaid on top. For Finnish topo maps, the reference is the printed 1:50,000 series from MML — a specific olive green for coniferous forest, a specific blue-grey for water. I sampled those colours directly from scanned topo sheets.

Choropleth maps (quantitative data) need sequential or diverging colour scales, not the rainbow palette that most people default to. The perceptually uniform scales from ColorBrewer (Viridis, Plasma, the sequential single-hue options) are correct for quantitative fills because they encode magnitude accurately as visual intensity. MapLibre's interpolate expression supports linear and step stops for implementing these.

Categorical maps (type-based) need hues that are clearly distinguishable but not garish. The challenge in MapLibre is that categorical fills interact with your basemap — a vibrant orange category colour that looks good on a white background may be unreadable over a textured forest polygon. Test your category colours over the actual backgrounds they'll appear on, not just in a legend swatch.

Road Width That Scales Correctly

The most common technical failure in MapLibre maps is road lines that are too thin at low zoom and too thick at high zoom, or that jump abruptly between sizes. The correct solution is an exponential interpolation expression on the line width property. Here's the pattern I use for the Finnish topographic road classification:

{
  "id": "roads-fill",
  "type": "line",
  "source": "mml-topo",
  "source-layer": "roads",
  "layout": {
    "line-cap": "round",
    "line-join": "round"
  },
  "paint": {
    "line-color": [
      "match", ["get", "luokka"],
      [1, 2], "#e8d5b0",
      [3, 4], "#f0e8d8",
      "#e8e8e8"
    ],
    "line-width": [
      "interpolate",
      ["exponential", 1.5],
      ["zoom"],
      5,  ["match", ["get", "luokka"],
            [1, 2], 1.5,
            [3, 4], 0.8,
            0.4],
      10, ["match", ["get", "luokka"],
            [1, 2], 4,
            [3, 4], 2.5,
            1.2],
      15, ["match", ["get", "luokka"],
            [1, 2], 14,
            [3, 4], 9,
            4],
      18, ["match", ["get", "luokka"],
            [1, 2], 28,
            [3, 4], 18,
            8]
    ]
  }
}

The ["exponential", 1.5] curve means the width grows faster than linearly as zoom increases, which mirrors the physical reality of what a road represents at different scales. At zoom 5 (country level) a motorway should be barely visible; at zoom 18 (street level) it should fill most of the screen. The 1.5 exponent is a good starting point; adjust it based on your specific data density.

Hillshading: Making Terrain Feel Real

The hillshade layer type is one of MapLibre's most underused features, partly because it requires a raster DEM source rather than a vector source, and most people's tile pipelines are vector-only.

To use hillshading, you need a raster-dem tile source. The source encoding can be Mapbox's own (which encodes elevation in RGB pixel values using a specific formula) or Terrarium format (which uses a different RGB formula and is used by Terrarium and AWS terrain tiles). These are not interchangeable — you must declare the correct encoding in your source definition:

"dem-source": {
  "type": "raster-dem",
  "tiles": ["https://your-tile-server/terrain/{z}/{x}/{y}.png"],
  "tileSize": 256,
  "encoding": "terrarium",
  "maxzoom": 12
}

The hillshade layer paint properties that produce naturalistic-looking relief are a matter of taste, but my starting configuration for Nordic terrain:

{
  "id": "hillshading",
  "type": "hillshade",
  "source": "dem-source",
  "paint": {
    "hillshade-exaggeration": 0.5,
    "hillshade-shadow-color": "#8a9a8a",
    "hillshade-highlight-color": "#ffffff",
    "hillshade-accent-color": "#5a6a5a",
    "hillshade-illumination-direction": 315
  }
}

The shadow colour using a slightly greenish-grey rather than pure grey or black is a cartographic convention that prevents hillshading from looking like a direct photo; the neutral green hue reads as terrain shadow rather than shadow from a light source. The exaggeration value of 0.5 is deliberately conservative for Finland's relatively gentle relief — increase it to 0.8–1.2 for alpine terrain.

Feature Filtering: The Power Tool

MapLibre's filter expression system lets you control which features from a source layer appear in a given rendering layer, based on their attributes and the current zoom level. Most tutorials show you the simple case: filter by a property value to show only motorways, or only parks. The more useful pattern combines zoom-dependent complexity with property filtering.

An example: in the Finnish MTK data, buildings have a kayttotarkoitus (purpose) attribute with dozens of codes. At low zoom levels, I only want to show substantial buildings (factories, large commercial buildings, prominent landmarks). At high zoom, I show everything. The combined filter:

"filter": [
  "all",
  [">=", ["zoom"], 14],
  ["any",
    [">", ["zoom"], 16],
    ["in", ["get", "kayttotarkoitus"],
      ["literal", [311, 312, 321, 322]]]
  ]
]

This shows all buildings at zoom 16 and above, but only the codes for industrial and commercial buildings between zoom 14 and 16. Below zoom 14, the layer is invisible. This zoom-dependent complexity management is what makes vector tile maps feel appropriately detailed at every zoom level rather than either overwhelming or sparse.

Performance Considerations

MapLibre performance depends significantly on how you organise your layers and sources. A few patterns that matter in practice:

  • Layer ordering is rendering order: MapLibre renders layers bottom-to-top. Put raster layers (hillshading, aerial imagery) at the bottom, fill polygons above that, lines above fills, symbols at the top. Symbol layers are expensive; minimise the number of distinct symbol layers.
  • Tile source maxzoom: Setting maxzoom on your tile source tells MapLibre to use the maxzoom tile for higher zoom levels, scaling it visually. This reduces tile requests significantly and works well for data that doesn't change character at high zoom (forest polygons, large water bodies).
  • Host glyphs and sprites locally: Fetching glyphs from a CDN adds latency for the first render. Self-hosting a small glyph set for the fonts you actually use eliminates this dependency and improves cold-start performance.
  • Buffer and tolerance in Tippecanoe: The tile buffer value controls how much data outside the tile boundary is included (needed for labels and features that span tile boundaries). Larger buffers mean larger tiles. A buffer of 64 is generally sufficient; the default of 128 can double tile sizes for dense data.

The Finnish Topographic Style

The style I built for the Finnish topo basemap makes a few deliberate decisions worth summarising. The land cover palette uses MML's own cartographic conventions: #c8d8b0 for coniferous forest, #d8e8c0 for deciduous, #e8d8c0 for open terrain, #b8d0d8 for water. These are slightly desaturated from the printed sheet originals to work better as a screen basemap under data overlays.

The 5-metre contour lines appear at zoom 13 with line-opacity: 0.4, increasing to 0.7 at zoom 15. The 25-metre index contours carry the elevation labels, rendered in a muted warm brown that reads as terrain annotation rather than cartographic noise. The label size for Finnish place names scales from 10px at zoom 8 to 16px at zoom 14, with a white halo at 1.5px throughout.

The result doesn't look like a Google Maps clone. That was the point. The complete style specification is available in the kherrala/mml-vectortiles repository.

The difference between a map that conveys information and a map that just displays data is almost entirely in the styling decisions. The data pipeline is engineering. The style is design. MapLibre gives you enough control to do the design work properly; most people stop before they get there.