Build a web map with MapLibre: production guide for interactive geospatial applications

MapLibre GL JS is a browser library for rendering interactive maps from vector tiles and other geographic data sources. A dependable MapLibre implementation requires more than creating a Map object: you must provide a valid style, choose and configure data sources, connect sources to layers, size the map container, preserve attribution, validate coordinate order, and operate the tile and data services behind the map. This guide builds a small web map with a custom style, a GeoJSON layer, feature interaction, and a provider-aware production workflow.

A MapLibre web map is a rendering application, not a complete geographic data system. MapLibre GL JS uses a style document to determine which sources to load, which layers to draw, their ordering, and how geographic features should appear. The style specification defines sources, layers, filters, layout properties, paint properties, and initial camera settings. (maplibre.org) That separation is useful. A team can change visual hierarchy without rewriting its geographic data pipeline, add a live GeoJSON overlay to an existing basemap, or replace a tile provider without redesigning every map component. It also creates clear production responsibilities: MapLibre renders and provides browser interaction, while your services provide authoritative features, attribution, freshness, access control, and geographic computation.

How EDK fits

For: Frontend developers, GIS engineers, and product teams building browser-based maps with MapLibre GL JS and separately operated basemap, vector-tile, raster, or GeoJSON services.

MapLibre is a focused presentation and interaction layer for a larger geographic system. It helps turn representations of physical features—such as roads, buildings, streams, parks, service requests, facilities, or moving assets—into an explorable map. The connection to E2 is strongest at the boundary between rendering and world-model computation: MapLibre can display the result of finding, filtering, ranking, measuring, comparing, or aggregating real-world features, while E2 represents the broader idea that those features should be searchable and computable through a common geographic interface. MapLibre itself does not supply E2's world model, universal feature coverage, or computation layer, and this guide does not attribute those capabilities to MapLibre.

  • •Render a computed geographic result—such as roads near a facility, buildings inside a service area, or infrastructure filtered by type—as a styled GeoJSON or vector-tile layer in a MapLibre interface.
  • •Use MapLibre as the visual front end for geographic workflows that rank, compare, aggregate, or detect change across physical features, while keeping search and computation in the appropriate data or spatial-analysis system.
  • •Display operational layers such as park-maintenance requests, sidewalk repairs, streams, schools, charging stations, or construction activity after an upstream system has validated their locations and selected the relevant geographic subset.
Primary role
Browser-side WebGL map rendering and interaction
Core configuration
A MapLibre Style Specification document plus one or more geographic sources
Common source types
Vector tiles, raster tiles, GeoJSON, raster elevation data, and other documented source types
Coordinate convention in examples
Longitude, latitude order for geographic positions
Production boundary
MapLibre does not provide the complete basemap, data licensing, geocoding, routing, or spatial-analysis backend

The EDK outcome

Before you start

A working JavaScript or TypeScript frontend project with a build tool such as Vite, webpack, or an equivalent environment.

Basic familiarity with HTML, CSS, JavaScript modules, JSON, and asynchronous network requests.

A MapLibre-compatible style URL or a style document you control.

A documented geographic data source and permission to use its tiles or features.

A clear understanding of coordinate order, CRS, feature properties, attribution, and update time for the data you plan to display.

Learning objectives

Initialize a MapLibre GL JS map with a valid style and correctly sized container.

Distinguish between styles, sources, source layers, and rendered layers.

Add and update a GeoJSON overlay with interaction.

Diagnose coordinate, source-layer, zoom, CORS, attribution, and provider failures.

Design a production boundary between browser rendering and geographic search, computation, and data operations.

Verification

The map container has a nonzero height at desktop and mobile breakpoints.

The style loads successfully and all required glyph, sprite, and tile resources return expected responses.

Every rendered layer references an existing source; vector-tile layers use the correct source-layer.

GeoJSON coordinates are in longitude, latitude order and geometries pass validation.

The map visibly presents required attribution and does not hide provider terms behind an inaccessible control.

Layer-specific clicks return the intended feature and do not expose unsanitized HTML from untrusted properties.

Empty, delayed, unauthorized, and failed data requests produce understandable UI states.

Large datasets are tested for network size, browser memory, frame rate, and interaction latency.

The geographic interpretation of displayed distances, rankings, boundaries, and timestamps is documented.

The application has a fallback path when the map or its provider cannot load.

Troubleshooting

Blank map or zero-height canvas: inspect the container's computed height, parent layout, CSS loading, and whether the map was initialized before the container became visible.

Style loads but the basemap is empty: inspect browser network requests for glyphs, sprites, TileJSON, and tile URLs. Check authentication, CORS, HTTPS, bounds, and provider coverage.

Vector layer does not appear: verify the source ID, exact source-layer, layer zoom range, filters, opacity, geometry type, and tile schema returned by the provider. Do not add source-layer to a GeoJSON layer. (maplibre.org)

GeoJSON request fails in the browser: check the response status, content type, CORS headers, relative URL resolution, and whether the server returns valid JSON rather than an HTML error page. (maplibre.org)

Features appear in the wrong place: check longitude and latitude order, coordinate transformation, axis-order assumptions in upstream services, and whether the data contains mixed CRS conventions.

Clicks return no feature: confirm the click handler uses the rendered layer ID, the feature is inside the query point, and the layer is visible at the current zoom.

Map becomes slow after repeated updates: avoid recreating sources and layers, debounce updates, reduce geometry, use clustering or tiles, and remove unused layers and listeners.

Attribution is missing or incorrect: inspect the style and source attribution configuration, then compare the displayed wording with the provider's license and usage requirements. For OpenStreetMap's standard tiles, follow the official policy exactly. (operations.osmfoundation.org)

A production provider blocks or throttles requests: review quotas, caching, request identification, traffic patterns, and whether a dedicated hosted or self-operated tile service is more appropriate.

A geographic measurement looks plausible but is wrong: move the computation out of the rendering layer, use the correct CRS and units for the question, and test against known control cases.

Next steps

Replace the placeholder style with a documented production style and record its version or revision.

Add a server-side endpoint that returns only the geographic features needed for the current map extent or query.

Introduce vector tiles when feature volume, geographic extent, or zoom-dependent generalization makes GeoJSON inefficient.

Add automated tests for coordinate order, source-layer names, feature properties, attribution, and empty-result behavior.

Connect the map to a search or computation service that can find, filter, rank, compare, or aggregate physical features before rendering them.

Provide a table, list, or accessible detail panel so the map is an interface to geographic information rather than the only representation of it.

What you need before writing code

Start by deciding what MapLibre will render and where each input will come from. A minimal application needs a browser project, the maplibre-gl package, MapLibre's CSS, a map container with an explicit height, and a style document. The style may be hosted by a provider or constructed by your team. MapLibre's official documentation and examples use this initialization model: provide a container, style, initial center, and zoom, then add sources and layers after the style is ready. (maplibre.org)

You also need a data plan. A vector-tile basemap may be supplied through a TileJSON URL or tile template. A custom overlay can be GeoJSON loaded from a URL or embedded in the page. When a browser fetches GeoJSON from another origin, the server must allow the request with suitable CORS headers. The MapLibre source specification supports inline GeoJSON and URL-based GeoJSON sources, but the source remains dependent on the availability and correctness of the upstream data service. (maplibre.org)

  • •Node.js and a browser-based JavaScript or TypeScript application.
  • •The maplibre-gl package and its CSS file.
  • •A style URL or a style object conforming to MapLibre Style Specification version 8. (maplibre.org)
  • •At least one geographic source: vector tiles, raster tiles, GeoJSON, raster elevation data, or another documented source type.
  • •A documented source of truth for feature geometry, properties, attribution, licensing, and update behavior.

Step 1: install MapLibre and create a known-good map

Install MapLibre in an existing frontend project, then import both the JavaScript library and its stylesheet. The CSS is part of the working map integration: without it, controls and map-related elements may not be presented correctly.

Before adding application data, prove that the container, style, camera, and browser network path work. A known-good base map gives you a reference point when a later failure comes from a GeoJSON endpoint, vector-tile schema, CORS policy, or layer definition rather than from MapLibre initialization.

  • •Run `npm install maplibre-gl`.
  • •Import `maplibre-gl` and `maplibre-gl/dist/maplibre-gl.css`.
  • •Give the map container a nonzero height at every responsive breakpoint.
  • •Use a provider style whose licensing and production terms you have reviewed.
  • •Keep the style URL in environment configuration rather than duplicating it across components.

Step 2: initialize the map with an explicit style and camera

The following example uses a style URL placeholder. Replace it with a style from a selected provider or with a style that your team controls. Geographic camera positions use longitude, latitude order in the example. The style itself determines the initial sources and layers, while later code can add application-specific sources and layers once the style has loaded.

Avoid placing secrets in a browser-visible style URL or tile URL unless the provider explicitly designs the credential for public client use. If a provider requires a private token, use its documented browser integration or place protected requests behind a server-side service.

  • •Initialize the map only after the container exists in the DOM.
  • •Use `attributionControl: true` unless your provider requires a different documented arrangement.
  • •Add navigation controls only after the basic map loads successfully.
  • •Record the style revision or provider configuration used by each deployment.
  • •Handle style and tile errors visibly instead of leaving users with a blank map.

Step 3: add a geographic overlay with GeoJSON

MapLibre's style model separates data from appearance. A GeoJSON source supplies features; a layer determines whether those features appear as circles, lines, fills, symbols, or another supported visual type. A source does not become visible until at least one layer references it. For vector tiles, the layer must also identify the correct source-layer inside the tile schema; that property is not used for GeoJSON sources. (maplibre.org)

For a custom result set—such as facilities returned from geographic search, roads selected by an analysis service, or buildings intersecting a study area—add a GeoJSON source and a circle layer after the style has loaded. The example uses a `category` property to show how application data can drive visual styling with an expression.

  • •Use `map.addSource()` to register the data source.
  • •Use `map.addLayer()` to define geometry type, styling, filtering, and draw order.
  • •Use expressions such as `['get', 'property_name']` to read feature properties.
  • •Use `map.on('click', 'layer-id', handler)` for layer-specific interaction.
  • •Use `setData()` on an existing GeoJSON source when the result set changes instead of recreating the entire map. MapLibre's GeoJSON source API documents this update pattern. (maplibre.org)

Step 4: make geographic behavior explicit

A visually correct map can still communicate the wrong geography. Treat coordinate order, reference systems, feature identity, units, zoom behavior, and temporal meaning as testable contracts. MapLibre can display the result of a geographic computation, but rendering does not validate whether the upstream computation used the right CRS, boundary version, timestamp, or spatial predicate.

For example, a map can show a circle around a facility without proving that the circle represents a 500-meter ground-distance buffer. It can show a route-shaped line without proving that the line is navigable, current, or appropriate for a particular vehicle profile. Those semantics belong to the data and computation systems that produced the geometry.

  • •Coordinate order: GeoJSON positions are `[longitude, latitude]`, not `[latitude, longitude]`.
  • •Coordinate reference system: transform projected coordinates before sending them to a source that expects geographic positions.
  • •Feature identity: stable IDs support selection, updates, hover state, and feature-state workflows.
  • •Units: screen distance is not ground distance; compute measurements in a system and CRS appropriate to the question.
  • •Zoom semantics: `minzoom` and `maxzoom` control layer visibility, while source coverage controls whether data can actually be requested.
  • •Vector-tile schema: a style layer must reference the exact `source-layer` name returned by the vector tiles. The property is required for vector-tile sources and prohibited for GeoJSON sources. (maplibre.org)
  • •Time: display observation time, update time, or data version when users could mistake an old feature for a current one.

Step 5: prepare for production operation

Separate the map renderer from the services that feed it. A production architecture commonly has a frontend, a style and tile provider, an API for application-specific features, and one or more systems for search, spatial computation, routing, or analytics. MapLibre can visualize the result, but it does not replace those systems.

Keep source URLs, API keys, attribution text, and environment-specific endpoints configurable. Add loading, empty, stale, and error states rather than leaving users with a blank canvas. Monitor style requests, tile errors, GeoJSON latency, payload size, browser memory, frame rate, and the percentage of users who reach a usable map.

  • •Keep container sizing and responsive behavior under automated UI tests.
  • •Validate the style before deployment and verify every vector source-layer name against the tile schema.
  • •Use provider-supported caching and authentication patterns; do not expose server-only secrets.
  • •Record data version or observation time for operational and time-sensitive features.
  • •Provide a non-map fallback for critical information, such as a list, table, address, or status message.
  • •Test empty results separately from failed requests so users can distinguish 'nothing matched' from 'the service is unavailable'.

Workflow

01

Install MapLibre and include its CSS

Install the package in your frontend project, then import both the JavaScript library and its stylesheet. The stylesheet supplies required map UI and canvas-related rules; omitting it can produce missing controls or incorrect presentation.

npm install maplibre-gl

// main.js or main.ts
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
02

Create a map container and initialize the camera

Give the container an explicit height and construct the map with a provider style. Keep the style endpoint in configuration rather than hard-coding it into many components.

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

<style>
  .map { width: 100%; height: 480px; }
</style>

const map = new maplibregl.Map({
  container: 'map',
  style: import.meta.env.VITE_MAP_STYLE_URL,
  center: [-73.9857, 40.7484],
  zoom: 12,
  attributionControl: true
});

map.addControl(new maplibregl.NavigationControl(), 'top-right');
03

Add a GeoJSON source and visual layer

Register a source after the style has loaded, then add a layer that references the source. This example expects a feature collection at `/data/features.geojson` with a `category` property.

map.on('load', () => {
  map.addSource('places', {
    type: 'geojson',
    data: '/data/features.geojson'
  });

  map.addLayer({
    id: 'places-circles',
    type: 'circle',
    source: 'places',
    paint: {
      'circle-radius': 6,
      'circle-color': [
        'match',
        ['get', 'category'],
        'park', '#2e8b57',
        'facility', '#2563eb',
        '#f59e0b'
      ],
      'circle-stroke-color': '#ffffff',
      'circle-stroke-width': 1
    }
  });

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

    new maplibregl.Popup()
      .setLngLat(event.lngLat)
      .setHTML(`<strong>${feature.properties?.name ?? 'Unnamed feature'}</strong>`)
      .addTo(map);
  });

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

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

Update data without rebuilding the map

When a server returns a new result set, update the existing GeoJSON source. This preserves the map, controls, camera, and layer configuration. Validate the response before setting it, and consider debouncing updates caused by rapid search or viewport changes.

async function refreshPlaces(url) {
  const response = await fetch(url, {
    headers: { 'Accept': 'application/geo+json, application/json' }
  });

  if (!response.ok) {
    throw new Error(`Feature request failed: ${response.status}`);
  }

  const geojson = await response.json();
  if (geojson.type !== 'FeatureCollection') {
    throw new Error('Expected a GeoJSON FeatureCollection');
  }

  const source = map.getSource('places');
  if (!source || typeof source.setData !== 'function') {
    throw new Error('GeoJSON source is not ready');
  }

  source.setData(geojson);
}
05

Validate the production contract

Before release, verify the style URL, tile and GeoJSON requests, CORS headers, attribution, source-layer names, camera bounds, coordinate order, layer visibility, error handling, and behavior when the provider is slow or unavailable. Test both a populated result and an empty result. Record the data timestamp and source in the user interface when freshness matters.

map.on('error', (event) => {
  console.error('MapLibre error', event.error);
});

map.on('sourcedata', (event) => {
  if (event.sourceId === 'places' && event.isSourceLoaded) {
    console.debug('Places source loaded');
  }
});

Questions teams ask

Does MapLibre include map tiles or a default production basemap?

No. MapLibre GL JS is a rendering library. You provide a style and the sources referenced by that style, such as vector tiles, raster tiles, or GeoJSON. The official documentation and examples demonstrate how to connect sources and layers but do not turn MapLibre into an authoritative geographic dataset or tile-hosting service. (maplibre.org)

Why does my vector source load but nothing appear?

Check that the style layer references the exact source ID, uses a supported layer type, and includes the correct `source-layer` name for the vector-tile layer. Then check layer zoom limits, filters, paint opacity, tile URL responses, bounds, and whether the camera is inside the source coverage. `source-layer` is required for vector-tile layers but prohibited for GeoJSON sources. (maplibre.org)

Can I use OpenStreetMap's standard tiles in a production MapLibre application?

Only within the OpenStreetMap Foundation's tile usage policy. The policy requires the correct HTTPS URL, visible attribution, appropriate request identification, caching, and no bulk downloading or offline prefetching. It also states that availability is best effort and provides no SLA. For substantial or commercial workloads, evaluate a suitable hosted provider or operate an appropriate tile service. (operations.osmfoundation.org)

Should I use GeoJSON or vector tiles for my overlay?

Use GeoJSON when the feature set is small or moderately sized, changes frequently, or needs straightforward application-controlled updates. Prefer vector tiles when the dataset is large, spans broad areas, must be generalized by zoom, or needs efficient viewport-based delivery. The correct choice depends on feature count, geometry complexity, update frequency, client memory, and the capabilities of the service producing the data.

Where should distance, proximity, or geographic ranking be computed?

Compute those results in the spatial database, analysis service, search system, or world-model layer that owns the relevant data and geographic semantics. Use MapLibre to display the result and support interaction. Screen pixels and map camera coordinates are not substitutes for a defensible geographic computation.

Keep exploring

Sources

More in Guides