Tutorial · Web GIS
Show your data on a web map with MapLibre
Add an interactive map, a GeoJSON layer, popups and data-driven colours to any web page.
Beginner12 min read

Key takeaways
- MapLibre GL JS is a free, open-source library for fast interactive vector maps.
- GeoJSON is a simple format for points, lines and polygons, and it uses [longitude, latitude] order.
- A source holds data, and a layer decides how it is drawn.
- Data-driven styling lets one layer show many colours based on a property.
A web map is the friendliest way to share spatial data. Anyone with a browser can zoom, click and explore, and nothing needs to be installed. MapLibre GL JS is a free, open-source library that draws smooth, interactive maps using the graphics card, and it works with any data you can express as GeoJSON.
In this tutorial you will put a map on a page, add a layer of areas from a GeoJSON file, colour the areas by a property, add popups and fit the view to your data. You need only a text editor and a browser.
What you need
- A text editor, and any modern browser.
- A GeoJSON file named areas.geojson with polygons that have a property called landuse, for example forest, water or farm. You can export one from QGIS or draw one at geojson.io.
- A way to serve files locally, because browsers block local files from loading data. In a terminal, run: python -m http.server 8000, then open http://localhost:8000.
Step 1: the page
Create an index.html file that loads the library from a CDN and reserves space for the map.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My first web map</title>
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<style>
body { margin: 0; }
#map { height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script src="map.js"></script>
</body>
</html>Step 2: create the map
In map.js, create the map. The style is the basemap. This demo style is free and good for learning. For a real project, choose a style from a provider whose terms fit your use and show its attribution.
const map = new maplibregl.Map({
container: "map",
style: "https://demotiles.maplibre.org/style.json",
center: [85.3237, 27.6808], // [longitude, latitude]
zoom: 10,
});
map.addControl(new maplibregl.NavigationControl());Step 3: add your data
Wait for the map to finish loading, then add a source that points to your file, and a layer that draws it as filled polygons. The color expression below is data-driven: it looks at each area’s landuse property and picks a colour.
map.on("load", () => {
map.addSource("areas", { type: "geojson", data: "areas.geojson" });
map.addLayer({
id: "areas-fill",
type: "fill",
source: "areas",
paint: {
"fill-color": [
"match", ["get", "landuse"],
"forest", "#2b5243",
"water", "#4a78a8",
"farm", "#c9b458",
"#999999", // anything else
],
"fill-opacity": 0.65,
},
});
map.addLayer({
id: "areas-outline",
type: "line",
source: "areas",
paint: { "line-color": "#111111", "line-width": 1 },
});
});Step 4: add popups
A map is more useful when people can click for details. We build the popup from plain text nodes rather than HTML strings. That keeps the page safe even if a property contains characters like angle brackets, and it matters whenever the data comes from outside.
map.on("click", "areas-fill", (e) => {
const props = e.features[0].properties;
const box = document.createElement("div");
box.textContent = `Land use: ${props.landuse}`;
new maplibregl.Popup().setLngLat(e.lngLat).setDOMContent(box).addTo(map);
});
// show a pointer cursor over clickable areas
map.on("mouseenter", "areas-fill", () => (map.getCanvas().style.cursor = "pointer"));
map.on("mouseleave", "areas-fill", () => (map.getCanvas().style.cursor = ""));Step 5: zoom to your data
Instead of guessing a centre and zoom level, calculate the bounds of your data and fit the view to them. This version reads the file once and works through every coordinate.
fetch("areas.geojson")
.then((r) => r.json())
.then((geojson) => {
const bounds = new maplibregl.LngLatBounds();
const extend = (coords) =>
Array.isArray(coords[0]) ? coords.forEach(extend) : bounds.extend(coords);
geojson.features.forEach((f) => extend(f.geometry.coordinates));
map.fitBounds(bounds, { padding: 40 });
});Make it ready for real use
- Attribution. Show the credit your basemap and data require. It is usually a licence condition.
- Large data. A GeoJSON file of tens of thousands of shapes will be slow. Convert it to vector tiles, which load only what is on screen.
- Accessibility. Add a text summary or table of the key information, so that the map is not the only way to read it.
- Mobile. Test on a phone, where the map should fill the screen and controls need to be easy to tap.
- Hosting. A map is just static files, so any static host will do.
Troubleshooting
- Blank grey map: open the browser console, and check that the style URL loaded.
- Data does not appear: confirm you added it inside the load event, and that the file path is correct.
- Points land in the wrong country: your coordinates are probably swapped to latitude, longitude.
- Shapes look distorted or offset: GeoJSON must be in standard WGS 84 coordinates. Reproject if it is not.
Adding a layer toggle
Once a map has more than one layer, people need a way to switch them on and off. A simple checkbox is enough. Add this to your page, and a few lines to map.js.
<label style="position:absolute;top:12px;left:12px;background:#fff;padding:6px 10px">
<input type="checkbox" id="toggle" checked /> Show areas
</label>document.getElementById("toggle").addEventListener("change", (e) => {
map.setLayoutProperty("areas-fill", "visibility", e.target.checked ? "visible" : "none");
map.setLayoutProperty("areas-outline", "visibility", e.target.checked ? "visible" : "none");
});The same pattern works for any layer. For many layers, build the checkboxes from a list, and keep their state in sync with the map.
Choosing a data and hosting strategy
- Small data, up to a few thousand shapes: a GeoJSON file served as a static asset, as in this tutorial.
- Medium data: simplify the geometry and reduce the decimal places in the coordinates, which can shrink files by half or more.
- Large data: convert to vector tiles with a tool such as tippecanoe, and serve them from static storage. Only the tiles in view are loaded.
- Live data: serve it from an API, and refresh the source with a timer or a stream.
- Hosting: a static host or a content delivery network is usually enough, and inexpensive.
Frequently asked questions
- Is MapLibre the same as Mapbox?
- MapLibre began as an open-source fork of an earlier version of Mapbox GL JS, and is developed by a community. It has no token requirement, and works with many tile providers.
- Can I use MapLibre with React?
- Yes. You create the map inside an effect and clean it up when the component is removed. Wrapper libraries exist, but the core code is the same as here.
- How do I show satellite imagery as the background?
- Use a style that includes a raster imagery source from a provider whose terms allow your use, and include the credit they require.
- Why is my map slow with a large file?
- Because the browser has to download and process every shape. Simplify the data, or switch to vector tiles.
You now have the foundation of almost every web map: a basemap, a source, a layer and some interaction. From here you can add filters, time sliders, legends and your own analysis results.
Join the conversation
Reactions & comments
Comments are opening soon.
In the meantime, tell us what you think by email at info.istem.lab@gmail.com.



