An Introduction to Mapbox GL JS for Custom Map Styling
Maps have come a long way from static images embedded in web pages. Today, developers can build richly interactive, pixel-perfect map experiences directly in the browser — and Mapbox GL JS is one of the most powerful tools for doing exactly that.
This article walks you through what Mapbox GL JS is, how it works under the hood, and how to use it to create custom-styled maps for your web projects.
What is Mapbox GL JS?
Mapbox GL JS is an open-source JavaScript library for rendering interactive maps in the browser using WebGL (Web Graphics Library). Unlike older mapping libraries that render maps as raster tile images (pre-drawn PNGs stitched together), Mapbox GL JS renders maps from vector tiles — compact binary data files that describe map features as geometric shapes.
This distinction matters because:
- Vector tiles are resolution-independent. They look sharp on any screen, from standard displays to 4K monitors and high-DPI mobile devices.
- Styles are applied client-side. The same geographic data can be rendered in a dozen different ways just by changing a style definition — no new data downloads required.
- Rendering is GPU-accelerated. WebGL uses the graphics card directly, enabling smooth 60fps pan, zoom, tilt, and rotation.
Mapbox GL JS is the backbone of Mapbox’s own mapping products, but it is also widely used independently via the Mapbox Maps SDK.
Core Concepts
Before writing any code, it helps to understand the three foundational concepts in Mapbox GL JS.
1. Styles
A style is a JSON document that defines everything about how a map looks — background color, which data sources to load, how roads and buildings are drawn, what fonts labels use, and more. Mapbox provides several pre-built styles (Streets, Outdoors, Satellite, Dark, Light), but you can write your own from scratch or customize existing ones.
A minimal style object looks like this:
{
"version": 8,
"name": "My Custom Style",
"sources": { ... },
"layers": [ ... ]
}
2. Sources
Sources tell the map where to get its geographic data. Common source types include:
| Source Type | Description |
|---|---|
vector | Mapbox vector tile URLs (.mvt or Mapbox tile URLs) |
raster | Raster tile image URLs |
geojson | Inline GeoJSON data or a URL to a .geojson file |
image | A single georeferenced image |
video | A georeferenced video |
Sources define what data exists. They do not define how it looks — that is the job of layers.
3. Layers
Layers are the visual rules applied to source data. Each layer references a source (and optionally a source layer within a vector tile), specifies a type, and defines paint and layout properties.
Layer types include:
fill— polygons (land, buildings, water bodies)line— roads, borders, pathssymbol— icons and text labelscircle— point dataraster— raster tile overlaysheatmap— density visualizationfill-extrusion— 3D extruded polygonssky— atmospheric sky renderingbackground— the map canvas background
Getting Started
Installation
You can load Mapbox GL JS via CDN or install it through npm.
CDN (simplest for getting started):
<link href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
npm:
npm install mapbox-gl
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
Access Token
Mapbox requires an access token to authenticate API requests. Sign up at mapbox.com to get one for free. Your token looks like:
pk.eyJ1IjoieW91cnVzZXJuYW1lIiwiYSI6ImNsZXhhbXBsZSJ9.xxxxx
Your First Map
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>My First Mapbox Map</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { width: 100%; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN_HERE';
const map = new mapboxgl.Map({
container: 'map', // ID of the container div
style: 'mapbox://styles/mapbox/streets-v12', // Style URL
center: [77.209, 28.614], // [longitude, latitude]
zoom: 12
});
</script>
</body>
</html>
This renders a fully interactive street map centered on New Delhi, India.
Custom Map Styling
This is where Mapbox GL JS truly shines. You have granular control over every visual element on the map.
Using Mapbox Studio
The easiest way to create a custom style is through Mapbox Studio — a browser-based visual editor at studio.mapbox.com. You can:
- Start from a base style and modify it visually
- Change colors, fonts, and icon sets
- Show or hide specific layers
- Publish your style and reference it via a
mapbox://styles/yourusername/styleidURL
Modifying Styles Programmatically
You can also modify a map’s style in code after it has loaded. This is useful for dynamic theming, user-driven customization, or data-driven styling.
Changing a layer’s paint property:
map.on('load', () => {
// Turn all water a custom teal color
map.setPaintProperty('water', 'fill-color', '#0d9488');
// Make roads thicker
map.setPaintProperty('road-primary', 'line-width', 6);
});
Changing a layer’s visibility:
// Hide all building footprints
map.setLayoutProperty('building', 'visibility', 'none');
// Show them again
map.setLayoutProperty('building', 'visibility', 'visible');
Adding Your Own GeoJSON Layer
You can overlay your own data on top of any base style.
map.on('load', () => {
// Add a GeoJSON source
map.addSource('my-points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: { type: 'Point', coordinates: [77.209, 28.614] },
properties: { name: 'India Gate' }
}
]
}
});
// Add a circle layer to render the points
map.addLayer({
id: 'my-points-layer',
type: 'circle',
source: 'my-points',
paint: {
'circle-radius': 8,
'circle-color': '#e11d48',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
});
Data-Driven Styling
One of the most powerful features of Mapbox GL JS is data-driven styling — the ability to style features based on their own properties using expressions.
map.addLayer({
id: 'population-circles',
type: 'circle',
source: 'cities',
paint: {
// Scale circle size based on a 'population' property
'circle-radius': [
'interpolate',
['linear'],
['get', 'population'],
1000, 4, // population 1,000 → radius 4px
100000, 20 // population 100,000 → radius 20px
],
// Color by category
'circle-color': [
'match',
['get', 'type'],
'capital', '#7c3aed',
'city', '#2563eb',
'town', '#16a34a',
/* default */ '#6b7280'
]
}
});
Expressions can reference feature properties, zoom level, map pitch, and more — giving you a rule engine for visual logic without writing a single event handler.
3D Terrain and Buildings
Mapbox GL JS supports true 3D rendering.
Extruded Buildings
map.on('load', () => {
map.addLayer({
id: '3d-buildings',
source: 'composite',
'source-layer': 'building',
filter: ['==', 'extrude', 'true'],
type: 'fill-extrusion',
minzoom: 15,
paint: {
'fill-extrusion-color': '#adb5bd',
'fill-extrusion-height': ['get', 'height'],
'fill-extrusion-base': ['get', 'min_height'],
'fill-extrusion-opacity': 0.8
}
});
});
Terrain
map.on('load', () => {
map.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14
});
map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 });
});
Interactivity
Maps become genuinely useful when users can interact with them.
Click Events on Layers
map.on('click', 'my-points-layer', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const name = e.features[0].properties.name;
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(`<strong>${name}</strong>`)
.addTo(map);
});
// Change cursor on hover
map.on('mouseenter', 'my-points-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'my-points-layer', () => {
map.getCanvas().style.cursor = '';
});
Flyto Animation
map.flyTo({
center: [72.877, 19.076], // Mumbai
zoom: 13,
pitch: 60,
bearing: -20,
duration: 3000 // milliseconds
});
Performance Tips
Mapbox GL JS is fast by default, but large datasets and complex styles benefit from a few best practices.
Use vector tiles over GeoJSON for large datasets. Tilesets are pre-processed and only load features visible in the viewport.
Cluster point data at low zoom levels. Mapbox supports built-in clustering on GeoJSON sources:
map.addSource('earthquakes', {
type: 'geojson',
data: 'earthquakes.geojson',
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
Set minzoom and maxzoom on layers. Avoid rendering fine detail layers at zoom levels where they aren’t visible anyway.
Avoid updating sources on every frame. For live data, throttle updates to a reasonable interval rather than streaming every change.
Summary
Mapbox GL JS gives developers a complete toolkit for building custom, interactive, and performant web maps. The key ideas to take away:
- Maps are defined by styles (JSON documents describing sources and layers)
- Sources provide geographic data; layers define how it looks
- Expressions unlock data-driven styling without JavaScript logic
- WebGL rendering makes 3D terrain, smooth animation, and large datasets practical in the browser
- Custom styles can be built visually in Mapbox Studio or programmatically via the API
Whether you are building a logistics dashboard, a real estate explorer, a GIS analysis tool, or a public data visualization, Mapbox GL JS gives you the control and performance to build it well.
