Web mapping has undergone a quiet revolution over the past decade. If you built map-based applications in the early 2010s, you were probably using WMS — the OGC Web Map Service standard — which works by sending a bounding box to a server, waiting for the server to render an image, and displaying that image in the browser. The experience was functional but sluggish: every pan and zoom required a round-trip to the server, and the server did significant work for each request.

Compare that to opening Google Maps or a modern web application built on MapLibre GL JS today. The map renders immediately, panning and zooming are fluid at 60 frames per second, and labels rotate and scale as you tilt the view. The transition from image tiles to vector tiles made this possible, and understanding how it works has become essential knowledge for anyone building spatial applications.

# What Are Vector Tiles?

A vector tile is a compact binary encoding of vector geographic data — points, lines, and polygons — cut to a specific tile in a standardised grid. Rather than sending rendered images, the server sends the raw geometry and attribute data for the features within each tile, and the rendering happens in the browser using WebGL.

The standard format is the Mapbox Vector Tile (MVT) specification, now an open standard, which encodes geometries and attributes in a Protocol Buffer format. A typical vector tile for an urban area at zoom level 14 might contain the geometries and attributes for every road, building, POI, and administrative boundary visible in that tile — typically compressed to between 10 and 100 kilobytes.

The rendering is handled by the client — in web applications, by MapLibre GL JS (or Mapbox GL JS) using WebGL shaders running on the GPU. Because the client has the raw data, it can:

  • Render at any resolution (retina displays, arbitrary window sizes) without requesting different tile sets
  • Apply arbitrary styles without re-fetching data
  • Rotate and tilt the map view with labels staying upright
  • Query features at the current zoom level interactively (hover to highlight, click to inspect)
  • Filter features by attribute without a server round-trip
  • Interpolate between zoom levels smoothly (no discrete jumps)

These capabilities transform the web map from a static display of pre-rendered cartography into an interactive, queryable interface to spatial data.

# The Tile Pyramid

Vector tiles are organised in a pyramid of zoom levels. At zoom level 0, a single tile covers the entire world. At zoom level 1, four tiles. At zoom level n, 4^n tiles. The commonly used range for web mapping is zoom 0 (world overview) to zoom 22 (sub-metre detail), though most data services stop at zoom 14–16 where individual buildings and streets are clearly distinguishable.

Each tile is identified by three numbers: the zoom level (z), the column (x), and the row (y) — typically written as z/x/y. A tile URL follows a pattern like https://example.com/tiles/{z}/{x}/{y}.mvt.

At lower zoom levels, data must be simplified or aggregated: rendering the geometries of every building in a country at zoom level 4 would produce both meaningless visual output and impossibly large tiles. Generating effective vector tiles requires careful consideration of which features to include at each zoom level, and how to simplify geometries appropriately.

# Generating Vector Tiles

# Tippecanoe

Tippecanoe, developed by Mapbox and now maintained as an open source project, is the standard tool for generating vector tile archives from GeoJSON, CSV, or FlatGeobuf input. It is extraordinarily configurable and handles the complex decisions about feature simplification, clustering, and dropping at different zoom levels automatically, with sensible defaults that work well for most datasets.

# Generate tiles from a GeoJSON file
tippecanoe -o output.mbtiles \
    --minimum-zoom=0 \
    --maximum-zoom=14 \
    --layer=buildings \
    buildings.geojson

# Generate tiles from multiple layers with specific zoom ranges
tippecanoe -o complete.mbtiles \
    --named-layer=countries:countries.geojson \
    --named-layer=cities:cities.geojson \
    --named-layer=roads:roads.geojson \
    --minimum-zoom=0 \
    --maximum-zoom=16

Tippecanoe writes output as MBTiles — a SQLite database containing the tiles — or to a directory structure. For serving from a tile server, MBTiles is convenient; for hosting on object storage, the directory structure is more practical.

# pg_tileserv

For dynamic vector tiles generated directly from PostGIS, pg_tileserv is a lightweight Go server that exposes PostGIS tables and functions as vector tile endpoints. It generates tiles on demand, making it suitable for data that changes frequently:

# Start pg_tileserv pointing at a PostGIS database
DATABASE_URL=postgres://user:pass@localhost/mydb pg_tileserv

With pg_tileserv running, every table with a geometry column in the database is automatically available as a tile endpoint at /[schema].[table]/{z}/{x}/{y}.pbf. This makes it very fast to go from a PostGIS table to a served tile layer.

The trade-off is performance under load: generating tiles on demand requires PostGIS queries for every tile request. For high-traffic production use, pre-generated tiles (Tippecanoe + object storage) are more appropriate; pg_tileserv is better for development, internal tools, and low-traffic data that changes often.

# PMTiles: Tiles Without a Server

PMTiles is one of the most significant recent innovations in web mapping infrastructure. It is a single-file archive format for pyramid tile archives — like MBTiles, but designed specifically to work with HTTP range requests from any standard web server or object storage service.

The key insight behind PMTiles is that HTTP range requests allow a client to read arbitrary byte ranges from a file without downloading the entire file. A PMTiles archive organises tiles so that tiles needed for a given view of the map are clustered together in the file, making range requests efficient. The client library (which is available for MapLibre GL JS, Leaflet, and other mapping libraries) handles the range requests transparently.

The consequence is remarkable: you can host a complete vector tile archive for an entire country on S3 or Cloudflare R2, serve it directly from that storage bucket, and a MapLibre GL JS client will render it correctly with no tile server required. The cost of serving a vector tile archive this way is essentially just the object storage and CDN egress costs — typically a few dollars per month even for moderately high traffic.

import { Protocol } from 'pmtiles';
import maplibregl from 'maplibre-gl';

// Register the PMTiles protocol handler
const protocol = new Protocol();
maplibregl.addProtocol('pmtiles', protocol.tile.bind(protocol));

// Create a map using a PMTiles archive hosted on S3
const map = new maplibregl.Map({
  container: 'map',
  style: {
    version: 8,
    sources: {
      'my-data': {
        type: 'vector',
        url: 'pmtiles://https://my-bucket.s3.amazonaws.com/my-data.pmtiles'
      }
    },
    layers: [/* your layer styles */]
  }
});

# MapLibre GL JS: The Rendering Engine

MapLibre GL JS is the open source JavaScript library that renders vector tiles using WebGL. It is the direct successor to Mapbox GL JS before the licence change in 2021 and has continued to receive active development under the MapLibre open governance organisation.

MapLibre GL JS renders maps by evaluating a JSON-based style specification that defines:

  • Sources: where to get the tile data (vector tiles, raster tiles, GeoJSON, images)
  • Layers: how to render the data (fill, line, symbol, circle, heatmap, fill-extrusion)
  • Styling expressions: data-driven styling based on feature properties

A minimal MapLibre GL JS map implementation:

import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [0, 30],
  zoom: 2
});

map.on('load', () => {
  // Add a custom data source
  map.addSource('my-layer', {
    type: 'vector',
    url: 'pmtiles://https://example.com/data.pmtiles'
  });

  // Add a fill layer
  map.addLayer({
    id: 'my-fill',
    type: 'fill',
    source: 'my-layer',
    'source-layer': 'data',
    paint: {
      'fill-color': [
        'interpolate', ['linear'],
        ['get', 'value'],
        0, '#00e5ff',
        100, '#ff4444'
      ],
      'fill-opacity': 0.7
    }
  });
});

The styling expression language is particularly powerful: it enables data-driven styling (colouring features by their attributes), zoom-dependent styling (showing more detail at higher zoom levels), and dynamic filtering — all computed in the browser without server involvement.

# The Complete Modern Web Mapping Stack

A modern, production-quality web mapping application typically combines these technologies:

  1. Data preparation: Tippecanoe generates optimised vector tiles from GeoPandas-cleaned source data
  2. Storage: PMTiles archive hosted on S3 or Cloudflare R2, served with appropriate CORS headers
  3. Rendering: MapLibre GL JS with a custom style specification
  4. Dynamic data: PostGIS + pg_tileserv for frequently-changing data that cannot be pre-tiled
  5. Basemap: Free vector basemap styles from MapTiler, Stadia Maps, or a self-hosted TileServer GL instance with OpenMapTiles data

This stack can be deployed entirely from open source, hosted on commodity cloud infrastructure, and is capable of serving millions of users per month for a few hundred dollars in infrastructure costs. It represents a fundamental shift from the expensive, complex map service infrastructure of the GIS platform era.

# Accessibility Considerations

Web maps built on vector tiles have a significant accessibility challenge: the map canvas itself is not accessible to screen readers, and spatial information is inherently difficult to convey through non-visual modalities.

Best practices for accessible web mapping include providing a non-map alternative for any map-presented information (a data table, a text description, a list), using aria-label attributes on map containers, implementing keyboard navigation for interactive map features, and ensuring that colour-only encoding (choropleth maps, heatmaps) is supplemented with other visual cues for users with colour vision deficiency.

The MapLibre GL JS addControl API makes it straightforward to add keyboard-accessible navigation controls, and the library emits accessible events that can be used to provide screen reader feedback.


Related reading: The Open Source Geospatial Stack: PostGIS, GDAL, and Beyond · Cloud-Orchestrated Geospatial Workflows · Low-Cost, High-Flexibility Spatial Architecture Patterns