Build a web map with MapLibre: a practical guide

MapLibre GL JS is an open-source TypeScript library that uses WebGL to render interactive maps in the browser. It provides the browser-side renderer, camera controls, style system, source and layer APIs, events, and interaction model. It does not automatically provide a complete basemap, geocoder, routing engine, or geographic dataset: those come from the style and services that you connect to it. This guide builds a small MapLibre map, adds a GeoJSON layer, explains how styles, sources, source layers, and rendered layers fit together, and identifies the operational decisions that matter when a prototype becomes a real geospatial application.

A useful MapLibre map is a visualization layer over geographic data. MapLibre GL JS reads a style document, requests the resources named by that style, and renders geographic features in the browser. That separation is important: MapLibre can display roads, buildings, boundaries, incidents, environmental features, public assets, or custom application data, but the underlying data, tile hosting, attribution, update process, and analytical meaning come from the services and datasets you connect to it. The practical workflow is therefore to establish a working renderer first, add a clearly defined source, add layers that communicate the data’s meaning, and then decide which searches or computations belong outside the browser.

How EDK fits

For: Frontend developers, GIS developers, data engineers, and product teams who need a provider-neutral starting point for building browser maps with MapLibre GL JS.

MapLibre is a useful presentation layer for a broader world-model workflow. It turns geographic sources into a visual, explorable representation of physical places, but it does not by itself create a unified representation of Earth or perform arbitrary geographic computation. The connection to E2 is strongest when a MapLibre map is used to inspect, filter, compare, or communicate relationships among real-world features such as roads, buildings, public assets, waterways, service requests, or infrastructure. In that setting, MapLibre supplies the interactive map interface while a world-model or geospatial computation layer supplies searchable features, normalized context, spatial relationships, measurements, rankings, and change signals behind the display. The distinction matters: a map renderer draws results; it is not automatically the system that discovers, reconciles, or computes those results.

  • •Use MapLibre as the browser view for a geographic search workflow: display a selected building, road, park, water feature, public asset, or service location, then let the surrounding application compute nearby features, filters, rankings, or regional summaries before returning map-ready data.
  • •Use a MapLibre layer stack to communicate computed geographic relationships, such as incidents near roads, maintenance requests within service areas, charging stations near facilities, or infrastructure intersecting a planning region. The computation should happen in the appropriate data or spatial-processing system; MapLibre can render the resulting GeoJSON or vector-tile layers.
  • •Use separate MapLibre layers to compare physical-world observations over time, such as construction activity, vegetation change, public-asset inventories, or changing road and land-use context, provided the underlying datasets include defensible timestamps and provenance.
Primary role
WebGL-based rendering of interactive maps in the browser.
Core configuration
A MapLibre style document containing sources, layers, expressions, and visual properties.
Common data formats
Vector tiles, raster tiles, GeoJSON, sprites, and glyph resources, depending on the style and source configuration.
Coordinate order
MapLibre examples use [longitude, latitude] for map centers and GeoJSON coordinates.
What it does not automatically provide
A complete basemap, geocoder, routing engine, universal place database, or general-purpose spatial analytics system.

The EDK outcome

Before you start

Basic HTML, CSS, and modern JavaScript or TypeScript.

Node.js and npm, or an equivalent frontend build environment.

A style URL or style JSON document that your application is permitted to use.

A geographic data source if you plan to add application-specific features.

An understanding that longitude comes before latitude in MapLibre’s coordinate examples.

A plan for attribution, licensing, data freshness, and tile or API rate limits.

Learning objectives

Install MapLibre GL JS and initialize a browser map.

Understand the relationship among styles, sources, source layers, and rendered layers.

Add GeoJSON features and style them with a MapLibre layer.

Recognize when vector tiles, filtering, clustering, or server-side aggregation are more appropriate than large inline GeoJSON.

Separate map rendering from geographic search, computation, routing, and data operations.

Verification

The map container has a visible height and the map canvas appears.

The style request succeeds in the browser network panel.

Glyph, sprite, raster, and tile requests succeed or fail with understood, actionable errors.

The initial center uses `[longitude, latitude]` and places the camera over the intended area.

The GeoJSON or vector source contains valid geometry and the expected property names.

Every rendered layer references the correct source, geometry type, and, for vector tiles, exact `source-layer`.

The map remains usable at representative zoom levels and on a slower network.

Attribution and licensing are visible wherever required by the data providers.

Troubleshooting

Blank map: inspect the container height first, then check whether the style URL returns valid JSON and whether the browser reports WebGL or worker errors.

Basemap without labels: inspect glyph URL requests and verify that the style’s font resources are available.

Layer does not render: confirm the source ID, source-layer name, geometry type, filters, zoom range, and camera coverage.

GeoJSON appears in the wrong place: verify coordinate order, geometry validity, and the coordinate reference system expected by GeoJSON and MapLibre.

Only some areas render: check tile coverage, provider limits, zoom-dependent source behavior, and whether the dataset is clipped or generalized.

A production build loads the map but no tiles: follow the official bundler guidance for MapLibre workers, especially when using ESM, Vite, Next.js, or a strict content-security policy. (maplibre.org)

Slow interaction: reduce feature count, simplify geometries, use vector tiles or server-side filtering, avoid repeatedly replacing large sources, and use zoom-dependent layers.

Unexpected styling: inspect feature properties and expression types. A numeric expression receiving strings, or a missing property, can make a layer appear empty or use fallback styling.

Incorrect interpretation: expose source dates, observation timestamps, units, and uncertainty rather than presenting every rendered feature as equally current or authoritative.

Next steps

Replace the demonstration style with a licensed production style and confirm its basemap, glyph, sprite, and tile dependencies.

Move substantial datasets from inline GeoJSON to vector tiles, filtered endpoints, or server-side aggregation.

Add a separate geographic service for search, routing, spatial joins, proximity, measurement, or regional aggregation.

Add accessibility features, keyboard-accessible controls, readable labels, and a non-map alternative for important information.

Add monitoring for style failures, tile errors, slow loads, source freshness, and provider quota or rate-limit responses.

Compare this standalone workflow with the React integration guide when the map becomes a component in a larger application.

What you need before writing code

You need a web project that can load JavaScript modules, the maplibre-gl package, MapLibre’s stylesheet, a map container with a non-zero height, and a style URL or inline style object. The official quickstart documents npm installation, module imports, the required CSS, and the basic Map constructor. (maplibre.org)

You also need to decide where the geographic content comes from. A style can reference vector tiles, raster tiles, GeoJSON, sprites, glyphs, and other resources. MapLibre renders those resources, but the library does not grant permission to use a particular provider’s tiles. Before shipping, check the provider’s terms, attribution requirements, rate limits, coverage, coordinate expectations, and production-use policy.

  • •Node.js and npm, or another JavaScript or TypeScript build environment.
  • •The maplibre-gl package and accompanying MapLibre CSS.
  • •A style URL or style JSON document. MapLibre styles use version 8 of the style specification. (maplibre.org)
  • •At least one geographic data source if you want to display application-specific features.
  • •A plan for attribution, data licensing, caching, updates, browser support, and error handling.

Step 1: install MapLibre and create a map container

Create a project with your usual frontend tooling, then install MapLibre GL JS. Import the Map class and the library stylesheet. The CSS is important for built-in controls and map UI elements, while the container needs an explicit height so the map canvas has visible space to occupy.

MapLibre’s current documentation describes an ESM-based setup and provides bundler guidance for common environments. If your framework performs server-side rendering, initialize the map only in a browser context and follow the relevant worker configuration for your bundler. (maplibre.org)

  • •Run `npm install maplibre-gl`.
  • •Import `Map` from `maplibre-gl` and import `maplibre-gl/dist/maplibre-gl.css`.
  • •Give the map container a fixed, responsive, or inherited height.
  • •Do not assume that a successful JavaScript import means that the style, tiles, glyphs, or sprites are reachable.

Step 2: initialize the smallest working map

The `Map` constructor needs a container and a style. The camera can be initialized with a longitude-latitude center and zoom level. MapLibre’s examples use coordinate order `[longitude, latitude]`, not `[latitude, longitude]`. (maplibre.org)

Start with the smallest possible map before adding application data. This isolates failures in the renderer, style URL, CSS, browser environment, and resource loading from failures in your own GeoJSON or vector-tile schema. The `load` event is a convenient point for adding application-specific sources and layers after the initial style is available.

  • •Use a stable container ID or DOM element reference.
  • •Start with a known style and a modest zoom level.
  • •Keep the initial camera over a recognizable location while debugging.
  • •Use browser network tools to verify the style request and its dependent resources.
  • •Treat a demonstration style as a development dependency, not as an implicit production data agreement.

Step 3: add a GeoJSON source and rendered layer

MapLibre separates data sources from visual layers. A GeoJSON source contains geographic features; a layer describes how those features should be drawn. The style specification defines source types and layer types separately, while the Map API exposes `addSource` and `addLayer` for constructing a map dynamically. (maplibre.org)

For a small set of points, an inline GeoJSON source is easy to understand. The same pattern can represent public assets, service locations, facilities, observations, or other point features. Give features meaningful properties because MapLibre expressions can read those properties to control color, radius, labels, visibility, and other visual behavior.

  • •Use a `circle` layer for simple point markers.
  • •Use a `symbol` layer when you need labels or icons.
  • •Use a `line` layer for roads, paths, routes, streams, or boundaries.
  • •Use a `fill` layer for polygons such as parks, parcels, service areas, or administrative regions.
  • •Use filters and zoom ranges so dense features do not appear at every scale.

Step 4: connect vector tiles and understand source-layer names

For larger datasets, use a vector-tile source rather than embedding a large GeoJSON object in the page. A vector source can reference a TileJSON document or a tile URL template. The source describes how tiles are fetched; the `source-layer` property identifies the named layer inside each vector tile. MapLibre’s style specification requires `source-layer` for layers using vector-tile sources and prohibits it for GeoJSON sources. (maplibre.org)

Confusing the source ID with the source-layer name is a common reason for a layer that appears configured but renders nothing. For example, a source may be registered under `transportation`, while the vector tiles inside it contain a source layer named `roads`. Those are different identifiers.

  • •Use a stable source ID such as `roads-source` and a separate rendered-layer ID such as `roads-lines`.
  • •Confirm the exact `source-layer` value in the TileJSON, style JSON, or provider documentation.
  • •Use `minzoom` and `maxzoom` to control when detailed layers appear.
  • •Use expressions for data-driven styling, but test them against real feature properties.
  • •Keep source IDs and layer IDs unique within the map style.

Step 5: add interaction without coupling the map to all application logic

MapLibre exposes events and feature-query methods for clicks, pointer movement, camera changes, and other map interactions. A common pattern is to query rendered features at the pointer location, read a small set of trusted properties, and update a separate application panel or URL state. The map should report which geographic feature was selected; the application should decide whether to retrieve related records, measurements, metadata, or additional places.

For a point layer, a popup can be created with the named `Popup` export. Avoid assuming a global `maplibregl` object when using named ESM imports. Keep business logic and expensive data retrieval outside the event handler where possible.

  • •Use a `click` handler for feature selection.
  • •Use hover interactions sparingly; frequent popups and state updates can become expensive.
  • •Use feature-state for transient visual state when appropriate instead of replacing a large source.
  • •Show timestamps, source labels, units, or uncertainty where they affect interpretation.
  • •Treat map rendering as separate from geocoding, routing, spatial joins, and geographic measurement.

Step 6: make the map production-aware

A prototype can load one style and a few features. A production map needs explicit decisions about resource availability, performance, accessibility, attribution, and failure states. A style is a dependency graph: the root style references sources, glyphs, sprites, and tile endpoints. A failure in any of those resources can produce missing labels, blank overlays, or partial rendering.

Test at the zoom levels and geographic areas your users actually need. A map that works over a small local extent may fail when the camera moves outside a provider’s coverage. Test slow networks, blocked requests, expired URLs, missing fonts, unsupported WebGL environments, and tile responses with unexpected schemas.

  • •Pin dependency versions where reproducibility matters.
  • •Use browser network tools to inspect style, glyph, sprite, and tile requests.
  • •Provide visible loading and error states instead of leaving a blank map.
  • •Verify attribution and licensing for every basemap and overlay source.
  • •Prefer vector tiles, filtered endpoints, or aggregation when a dataset is too large for one browser payload.
  • •Measure rendering and interaction performance on representative devices, not only a development laptop.
  • •Provide a non-map representation for important information when accessibility or degraded rendering requires it.

Workflow

01

Install the dependency

Install MapLibre GL JS in your project and import its CSS. The CSS is required for built-in map controls and related UI elements to display correctly.

npm install maplibre-gl

// src/main.js
import { Map, Popup } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
02

Create a container and initialize the map

Give the map element a real height, then construct a Map object with a style URL, center, and zoom. Use the demonstration style only for learning; choose and validate a production style before deployment.

<div id="map"></div>

<style>
  #map { height: 480px; }
</style>

<script type="module">
  import { Map } from 'maplibre-gl';
  import 'maplibre-gl/dist/maplibre-gl.css';

  const map = new Map({
    container: 'map',
    style: 'https://demotiles.maplibre.org/globe.json',
    center: [-73.9857, 40.7484],
    zoom: 10
  });
</script>
03

Add a GeoJSON source and layer after the style loads

Define a GeoJSON FeatureCollection, register it with addSource, and reference it from a circle layer. For production data, use stable property names and make timestamps or provenance available when they affect interpretation.

map.on('load', () => {
  map.addSource('places', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          properties: { name: 'Example place', category: 'demo' },
          geometry: { type: 'Point', coordinates: [-73.9857, 40.7484] }
        }
      ]
    }
  });

  map.addLayer({
    id: 'places-points',
    type: 'circle',
    source: 'places',
    paint: {
      'circle-radius': 7,
      'circle-color': '#2563eb',
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff'
    }
  });
});
04

Add a click interaction

Query the rendered layer at the click location and use the returned properties to update a popup or application panel. Named ESM imports keep the example consistent with the npm setup, and expensive data retrieval or geographic computation should remain outside the click handler.

map.on('click', 'places-points', (event) => {
  const feature = event.features?.[0];
  if (!feature) return;

  const name = feature.properties?.name ?? 'Unnamed feature';
  new Popup()
    .setLngLat(event.lngLat)
    .setText(name)
    .addTo(map);
});

map.on('mouseenter', 'places-points', () => {
  map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'places-points', () => {
  map.getCanvas().style.cursor = '';
});
05

Replace the demo source with a production data service

Move from inline GeoJSON to a hosted GeoJSON endpoint, TileJSON document, or vector-tile template when the dataset grows. Confirm the source schema, coverage, update behavior, attribution, and access policy before wiring it into the style. For vector tiles, source-layer must match the internal layer name in the tiles.

map.addSource('roads', {
  type: 'vector',
  url: 'https://your-authorized-provider.example/roads.json'
});

map.addLayer({
  id: 'roads-line',
  type: 'line',
  source: 'roads',
  'source-layer': 'roads',
  paint: {
    'line-color': '#475569',
    'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 16, 5]
  }
});

Questions teams ask

Does MapLibre provide map data by itself?

No. MapLibre GL JS is a map renderer and interaction library. The map’s style references resources such as vector tiles, raster tiles, glyphs, sprites, or GeoJSON. You must choose appropriate providers or host your own data, and you remain responsible for licensing, attribution, availability, and freshness.

Should I use GeoJSON or vector tiles?

Use GeoJSON for small or moderate datasets that are easy to retrieve and update as one collection. Use vector tiles when the data covers a large area, contains many features, must work across multiple zoom levels, or should be fetched incrementally. The decision depends on feature count, geometry complexity, update frequency, filtering needs, and supported devices.

Why is my MapLibre layer not visible?

Check the browser console and network panel first. Then verify that the style loaded, the source request succeeds, the layer references the correct source ID, the vector layer uses the exact `source-layer` name, the geometry type matches the layer type, the camera is within the data coverage, and `minzoom`, `maxzoom`, filters, and paint properties are not hiding the features.

Why is the map blank even though the JavaScript runs?

Common causes include a map container with no height, a failed or inaccessible style URL, blocked tile or glyph requests, malformed style JSON, unsupported browser rendering, or a bundler and worker configuration problem. MapLibre’s documentation includes bundler-specific worker guidance for ESM builds and strict content-security policies. (maplibre.org)

Can MapLibre calculate distance, routes, or nearby features?

MapLibre can display the results of those computations and support interaction around rendered features, but it is not itself a general routing engine or spatial database. Perform substantial geographic computation in a suitable backend or analytical system, then return map-ready features, tiles, or summaries.

Keep exploring

Sources

More in Guides