Skip to content

Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid

FixDevs ·

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 visible

Or the access token is rejected:

Error: A valid Mapbox access token is required

Or markers appear in the wrong position:

Marker shows up in the ocean instead of New York City

Why 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 div with no height renders a zero-height map. The container must exist in the DOM before new 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,
  },
};
'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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles