Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
Quick Answer
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
The Problem
The map container is empty:
import mapboxgl from 'mapbox-gl';
mapboxgl.accessToken = 'pk.xxx';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
});
// Empty div — no map tiles visibleOr the access token is rejected:
Error: A valid Mapbox access token is requiredOr markers appear in the wrong position:
Marker shows up in the ocean instead of New York CityWhy This Happens
Mapbox GL JS renders vector maps using WebGL. Common issues stem from:
- The access token must be valid and active — Mapbox requires an API key for all map renders. Free tier includes 50,000 loads/month. An expired, deleted, or mistyped token shows no map.
- The container element must have explicit dimensions — Mapbox fills its container. A
divwith no height renders a zero-height map. The container must exist in the DOM beforenew Map()is called. - Coordinates are
[longitude, latitude]— Mapbox uses[lng, lat]order, which is the opposite of Google Maps ([lat, lng]). New York is[-74.006, 40.7128], not[40.7128, -74.006]. - Mapbox GL JS uses WebGL — it requires a browser with WebGL support. Server-side rendering fails because there’s no WebGL context. In Next.js, the map must be client-only.
Fix 1: React Integration with react-map-gl
npm install react-map-gl mapbox-gl'use client';
import Map, { Marker, Popup, NavigationControl, GeolocateControl } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useState } from 'react';
const MAPBOX_TOKEN = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;
function MapView() {
const [viewState, setViewState] = useState({
longitude: -74.006,
latitude: 40.7128,
zoom: 12,
});
return (
<div style={{ width: '100%', height: '500px' }}>
<Map
{...viewState}
onMove={(evt) => setViewState(evt.viewState)}
mapboxAccessToken={MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
// Other map styles:
// 'mapbox://styles/mapbox/dark-v11'
// 'mapbox://styles/mapbox/light-v11'
// 'mapbox://styles/mapbox/satellite-v9'
// 'mapbox://styles/mapbox/satellite-streets-v12'
>
{/* Navigation controls (zoom +/-) */}
<NavigationControl position="top-right" />
{/* User location button */}
<GeolocateControl position="top-right" trackUserLocation />
{/* Marker */}
<Marker longitude={-74.006} latitude={40.7128} anchor="bottom">
<div style={{ fontSize: '24px' }}>📍</div>
</Marker>
</Map>
</div>
);
}Fix 2: Markers with Popups
'use client';
import Map, { Marker, Popup } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useState } from 'react';
interface Location {
id: string;
name: string;
description: string;
longitude: number;
latitude: number;
}
const locations: Location[] = [
{ id: '1', name: 'Central Park', description: 'Famous park in Manhattan', longitude: -73.9654, latitude: 40.7829 },
{ id: '2', name: 'Brooklyn Bridge', description: 'Historic bridge', longitude: -73.9969, latitude: 40.7061 },
{ id: '3', name: 'Times Square', description: 'The crossroads of the world', longitude: -73.9855, latitude: 40.7580 },
];
function MapWithPopups() {
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null);
return (
<div style={{ width: '100%', height: '600px' }}>
<Map
initialViewState={{ longitude: -73.98, latitude: 40.75, zoom: 12 }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
{locations.map(loc => (
<Marker
key={loc.id}
longitude={loc.longitude}
latitude={loc.latitude}
anchor="bottom"
onClick={(e) => {
e.originalEvent.stopPropagation();
setSelectedLocation(loc);
}}
>
<div style={{
width: '30px', height: '30px', borderRadius: '50%',
background: '#3b82f6', border: '3px solid white',
boxShadow: '0 2px 6px rgba(0,0,0,0.3)', cursor: 'pointer',
}} />
</Marker>
))}
{selectedLocation && (
<Popup
longitude={selectedLocation.longitude}
latitude={selectedLocation.latitude}
anchor="bottom"
onClose={() => setSelectedLocation(null)}
closeOnClick={false}
offset={25}
>
<div style={{ padding: '8px' }}>
<h3 style={{ margin: '0 0 4px', fontWeight: 'bold' }}>{selectedLocation.name}</h3>
<p style={{ margin: 0, fontSize: '14px', color: '#666' }}>{selectedLocation.description}</p>
</div>
</Popup>
)}
</Map>
</div>
);
}Fix 3: Custom Layers and Data Visualization
'use client';
import Map, { Source, Layer } from 'react-map-gl';
import type { CircleLayer, FillLayer, LineLayer } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
// GeoJSON data
const geojsonData: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: { type: 'Point', coordinates: [-74.006, 40.7128] },
properties: { name: 'NYC', magnitude: 8 },
},
{
type: 'Feature',
geometry: { type: 'Point', coordinates: [-118.2437, 34.0522] },
properties: { name: 'LA', magnitude: 5 },
},
],
};
const circleLayer: CircleLayer = {
id: 'points',
type: 'circle',
paint: {
'circle-radius': ['*', ['get', 'magnitude'], 4],
'circle-color': '#3b82f6',
'circle-opacity': 0.7,
'circle-stroke-width': 2,
'circle-stroke-color': '#fff',
},
};
function DataMap() {
return (
<Map
initialViewState={{ longitude: -98, latitude: 39, zoom: 3 }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/dark-v11"
style={{ width: '100%', height: '600px' }}
>
<Source id="data" type="geojson" data={geojsonData}>
<Layer {...circleLayer} />
</Source>
</Map>
);
}
// Heatmap layer
const heatmapLayer: HeatmapLayer = {
id: 'heatmap',
type: 'heatmap',
paint: {
'heatmap-weight': ['get', 'magnitude'],
'heatmap-intensity': 1,
'heatmap-radius': 30,
'heatmap-opacity': 0.8,
},
};Fix 4: Geocoding (Search)
'use client';
import Map, { Marker } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useState } from 'react';
function MapWithSearch() {
const [searchResult, setSearchResult] = useState<{ lng: number; lat: number } | null>(null);
const [query, setQuery] = useState('');
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
if (!query) return;
const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?access_token=${process.env.NEXT_PUBLIC_MAPBOX_TOKEN}&limit=1`
);
const data = await res.json();
if (data.features?.[0]) {
const [lng, lat] = data.features[0].center;
setSearchResult({ lng, lat });
}
}
return (
<div>
<form onSubmit={handleSearch} className="mb-4">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for a place..."
className="border rounded px-3 py-2 mr-2"
/>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Search
</button>
</form>
<div style={{ height: '500px' }}>
<Map
initialViewState={{
longitude: searchResult?.lng || -74.006,
latitude: searchResult?.lat || 40.7128,
zoom: searchResult ? 14 : 10,
}}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
{searchResult && (
<Marker longitude={searchResult.lng} latitude={searchResult.lat} />
)}
</Map>
</div>
</div>
);
}Fix 5: Next.js Configuration
// Mapbox GL JS is client-only — use dynamic import
import dynamic from 'next/dynamic';
const MapView = dynamic(() => import('@/components/MapView'), {
ssr: false,
loading: () => <div style={{ height: 500, background: '#f0f0f0' }}>Loading map...</div>,
});
export default function Page() {
return <MapView />;
}
// Or use 'use client' directive
// components/MapView.tsx
'use client';
// ... map component with react-map-gl/* Ensure mapbox-gl CSS is loaded */
/* Import in your global CSS or component: */
/* @import 'mapbox-gl/dist/mapbox-gl.css'; */
/* Or in layout.tsx: */
/* import 'mapbox-gl/dist/mapbox-gl.css'; */Fix 6: Fit Bounds to Data
import Map, { Marker, useMap } from 'react-map-gl';
import { useEffect } from 'react';
function FitToMarkers({ locations }: { locations: Location[] }) {
const { current: map } = useMap();
useEffect(() => {
if (!map || locations.length === 0) return;
const bounds = new mapboxgl.LngLatBounds();
locations.forEach(loc => bounds.extend([loc.longitude, loc.latitude]));
map.fitBounds(bounds, { padding: 50, duration: 1000 });
}, [map, locations]);
return (
<>
{locations.map(loc => (
<Marker key={loc.id} longitude={loc.longitude} latitude={loc.latitude} />
))}
</>
);
}Still Not Working?
“A valid Mapbox access token is required” — the token is missing or invalid. Get a token from mapbox.com → Account → Tokens. Use NEXT_PUBLIC_MAPBOX_TOKEN (with NEXT_PUBLIC_ prefix) so it’s available client-side.
Map container is empty with no errors — the container div has zero height. Set style={{ height: '500px' }} on the Map component or its parent. Also import mapbox-gl/dist/mapbox-gl.css — without it, map tiles load but controls and popups are broken.
Markers in the wrong place — Mapbox uses [longitude, latitude] order. New York is longitude: -74.006, latitude: 40.7128. If you swap them, the marker ends up in Central Asia.
Map flickers or re-renders constantly — the initialViewState object is created on every render, causing the map to reset. Use useState to manage view state, or define it outside the component. Never create the initial view state inline in JSX.
For related frontend issues, see Fix: React Three Fiber Not Working and Fix: Next.js App Router Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
Fix: Million.js Not Working — Compiler Errors, Components Not Optimized, or React Compatibility Issues
How to fix Million.js issues — compiler setup with Vite and Next.js, block() optimization rules, component compatibility constraints, automatic mode, and debugging performance gains.
Fix: Radix UI Not Working — Popover Not Opening, Dialog Closing Immediately, or Styling Breaking
How to fix Radix UI issues — Popover and Dialog setup, controlled vs uncontrolled state, portal rendering, animation with CSS or Framer Motion, accessibility traps, and Tailwind CSS integration.