Mapbox in a single HTML file: the script, the token and the map

A Mapbox map in HTML needs three things: the Mapbox GL JS script and CSS, your own public access token, and a div with a size. The pieces below run in a plain file, and the token is the step that needs your own account.

"Mapbox API" can mean two things. Here it means Mapbox GL JS, the browser library that draws an interactive map from a script tag. The Mapbox web services, such as geocoding, are separate HTTP endpoints, covered in the FAQ.

A map page needs three things: the library's script and CSS, a Mapbox access token, and a <div> with a size. Without the token the library loads and then refuses to draw.

Try that part first. This example loads Mapbox GL JS 3.32.0 from unpkg and asks for a map with no token.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mapbox GL JS without a token</title>
<link href="https://unpkg.com/mapbox-gl@3.32.0/dist/mapbox-gl.css" rel="stylesheet">
<script src="https://unpkg.com/mapbox-gl@3.32.0/dist/mapbox-gl.js"></script>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h1 { font-size: 17px; margin: 0 0 10px; }
  .row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 10px; }
  button { font: inherit; padding: 8px 14px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
  #map { height: 120px; border-radius: 10px; background: #dbe4f0; margin-bottom: 10px; }
  .box { background: #fff; border-radius: 10px; padding: 10px 12px; font-size: 14px; line-height: 1.5; margin-bottom: 8px; }
  .ok { color: #0f5132; } .bad { color: #9a3412; }
  code { font: 12.5px ui-monospace, Consolas, monospace; background: #eef1f5; padding: 0 3px; border-radius: 4px; }
</style>
</head>
<body>
<h1>Mapbox GL JS: library vs. token</h1>
<div class="box" id="lib">Loading the library...</div>
<div class="row"><button id="go" type="button">Create a map with no token</button></div>
<div id="map"></div>
<div class="box" id="out">Press the button. The library is loaded, but no map can be drawn without a token.</div>

<script>
  const lib = document.getElementById('lib');
  const out = document.getElementById('out');

  if (typeof mapboxgl === 'undefined') {
    lib.innerHTML = '<span class="bad">The script did not load (blocked or offline).</span>';
  } else {
    lib.innerHTML = '<span class="ok">Library loaded.</span> <code>mapboxgl.version</code> = ' +
      mapboxgl.version + ', <code>mapboxgl.supported()</code> = ' + mapboxgl.supported();
  }

  document.getElementById('go').addEventListener('click', () => {
    if (typeof mapboxgl === 'undefined') return;
    out.textContent = 'Creating the map...';
    try {
      const map = new mapboxgl.Map({
        container: 'map',
        // an inline style with no Mapbox-hosted data, and no accessToken on purpose
        style: { version: 8, sources: {}, layers: [] },
        center: [30, 15],
        zoom: 1
      });
      map.on('error', (e) => {
        out.innerHTML = '<span class="bad">error event:</span> ' + e.error.message;
      });
    } catch (err) {
      out.innerHTML = '<span class="bad">thrown:</span> ' + err.message;
    }
  });
</script>
</body>
</html>
The library loads and reports its version. Creating a map with no token produces the library's own error message.

What a Mapbox page needs

The three parts of a Mapbox page: library files, a public token, and a container with a size.
The three parts of a Mapbox page: library files, a public token, and a container with a size.

This is the shape of the example in the Mapbox documentation, with a pinned version in the addresses:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.32.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.32.0/mapbox-gl.js"></script>
<style>
  body { margin: 0; padding: 0; }
  #map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
  const map = new mapboxgl.Map({
    accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
    container: 'map',
    style: 'mapbox://styles/mapbox/standard',
    zoom: 1,
    center: [30, 15]
  });
</script>
</body>
</html>

The library is also an npm package, so the same files are on unpkg under mapbox-gl@3.32.0/dist/.

Keep the library's script tag above your own script. A classic script runs as soon as the browser reaches it. Add a viewport meta tag so the map is sized correctly on a phone.

The access token

The accessToken option on the Map is what ties the map to your Mapbox account. Create the token in your account and paste it in place of the placeholder.

Mapbox has two kinds. A public token starts with pk. and is designed for web pages and apps. A secret token starts with sk. and is for servers only, so it never belongs in HTML.

On a public token you can add URL restrictions. The token then works only for requests that come from the addresses you list, and an unrestricted token works from any address.

The Mapbox pricing page lists up to 50,000 map loads a month for Mapbox GL JS. A map load is counted each time a Map object is initialized.

Panning and zooming afterwards are not counted again. So create one Map per page, not one per click.

Version 2 and later of the library is licensed under the Mapbox Terms of Service and needs an active Mapbox account. Versions 1.13 and earlier use the BSD-3-Clause licence.

Longitude first, then latitude

Mapbox GL JS writes a coordinate as [longitude, latitude], to match GeoJSON. Many map apps show the pair the other way round, so a copied number pair is a common source of a map in the wrong ocean.

The same two numbers in both orders. Only longitude first is a valid point for the library.
The same two numbers in both orders. Only longitude first is a valid point for the library.

The library checks latitude for you. Swap the numbers of point A below and watch the error. The page uses the library's LngLat class, which needs no token.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Longitude, latitude order</title>
<script src="https://unpkg.com/mapbox-gl@3.32.0/dist/mapbox-gl.js"></script>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h1 { font-size: 17px; margin: 0 0 4px; }
  p.sub { margin: 0 0 12px; font-size: 13.5px; color: #4b5563; }
  .grid { display: grid; grid-template-columns: auto 1fr 1fr; gap: 6px 8px; align-items: center; font-size: 14px; }
  .grid b { font-weight: 600; }
  input { width: 100%; box-sizing: border-box; font: inherit; padding: 7px 8px; border: 1px solid #cfd4dc; border-radius: 8px; }
  .head { font-size: 12px; color: #6b7280; }
  .row { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; }
  button { font: inherit; padding: 8px 12px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
  button.alt { background: #e5e7eb; color: #1d2330; }
  .box { background: #fff; border-radius: 10px; padding: 10px 12px; font-size: 14px; line-height: 1.55; }
  .ok { color: #0f5132; } .bad { color: #9a3412; }
  code { font: 12.5px ui-monospace, Consolas, monospace; background: #eef1f5; padding: 0 3px; border-radius: 4px; }
</style>
</head>
<body>
<h1>Coordinates are [longitude, latitude]</h1>
<p class="sub">Mapbox GL JS uses the GeoJSON order. Swap the two and see what the library says.</p>

<div class="grid">
  <span></span><span class="head">longitude</span><span class="head">latitude</span>
  <b>A</b><input id="aLng" value="127" inputmode="decimal" aria-label="A longitude"><input id="aLat" value="37.5" inputmode="decimal" aria-label="A latitude">
  <b>B</b><input id="bLng" value="0" inputmode="decimal" aria-label="B longitude"><input id="bLat" value="0" inputmode="decimal" aria-label="B latitude">
</div>
<div class="row">
  <button id="swap" type="button">Swap A's two numbers</button>
  <button id="wrap" class="alt" type="button">Try A longitude 190</button>
  <button id="reset" class="alt" type="button">Reset</button>
</div>
<div class="box" id="out"></div>

<script>
  const $ = (id) => document.getElementById(id);
  const out = $('out');

  function show() {
    if (typeof mapboxgl === 'undefined') { out.textContent = 'The library did not load.'; return; }
    const lines = [];
    try {
      const a = new mapboxgl.LngLat(parseFloat($('aLng').value), parseFloat($('aLat').value));
      const b = new mapboxgl.LngLat(parseFloat($('bLng').value), parseFloat($('bLat').value));
      lines.push('<span class="ok">A =</span> <code>[' + a.toArray().join(', ') + ']</code>');
      lines.push('A wrapped: <code>[' + a.wrap().toArray().join(', ') + ']</code>');
      lines.push('Distance A to B: ' + Math.round(a.distanceTo(b) / 1000) + ' km (approximate)');
    } catch (err) {
      lines.push('<span class="bad">Error:</span> ' + err.message);
    }
    out.innerHTML = lines.join('<br>');
  }

  $('swap').addEventListener('click', () => {
    const lng = $('aLng').value;
    $('aLng').value = $('aLat').value;
    $('aLat').value = lng;
    show();
  });
  $('wrap').addEventListener('click', () => { $('aLng').value = '190'; $('aLat').value = '37.5'; show(); });
  $('reset').addEventListener('click', () => {
    $("aLng").value = "127"; $("aLat").value = "37.5";
    $('bLng').value = '0'; $('bLat').value = '0';
    show();
  });
  document.querySelectorAll('input').forEach((i) => i.addEventListener('input', show));
  show();
</script>
</body>
</html>
LngLat takes longitude then latitude. A latitude outside -90 to 90 throws, and a longitude of 190 wraps around.

The distance in the example is "approximate", in the words of the distanceTo documentation, and it is returned in metres.

Add a marker and a popup

After the Map exists, a marker is one chained call. Give it a position with setLngLat, then attach it with addTo.

const marker = new mapboxgl.Marker({ color: '#2563eb' })
  .setLngLat([30.5, 50.5])
  .addTo(map);

// bind a popup to the marker
marker.setPopup(new mapboxgl.Popup().setHTML('<b>Hello</b>'));

Marker takes options such as color and draggable. A Popup can also be placed on its own with setLngLat and addTo.

Build the whole page

This builder writes the complete file for you. Paste your own public token, choose the library address, and copy the result. A sk. token is refused.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mapbox page builder</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h1 { font-size: 17px; margin: 0 0 4px; }
  p.sub { margin: 0 0 12px; font-size: 13.5px; color: #4b5563; }
  label { display: block; font-size: 12.5px; color: #4b5563; margin: 8px 0 3px; }
  select { width: 100%; box-sizing: border-box; font: inherit; padding: 7px 8px; border: 1px solid #cfd4dc; border-radius: 8px; background: #fff; }
  input { width: 100%; box-sizing: border-box; font: inherit; padding: 7px 8px; border: 1px solid #cfd4dc; border-radius: 8px; }
  .three { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
  #warn { min-height: 20px; font-size: 13px; margin-top: 6px; }
  .ok { color: #0f5132; } .bad { color: #9a3412; }
  textarea { width: 100%; box-sizing: border-box; height: 210px; margin-top: 10px; font: 12px/1.45 ui-monospace, Consolas, monospace; padding: 8px; border: 1px solid #cfd4dc; border-radius: 8px; resize: vertical; }
  button { font: inherit; margin-top: 8px; padding: 8px 14px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<h1>Build the single-file map page</h1>
<p class="sub">Fill in your own public token. The page text below updates as you type.</p>

<label for="tok">Your Mapbox public access token</label>
<input id="tok" placeholder="pk.your-token-here" autocomplete="off" spellcheck="false">
<div id="warn"></div>

<label for="cdn">Load the library from</label>
<select id="cdn">
  <option value="mapbox">Mapbox's own CDN (api.mapbox.com)</option>
  <option value="unpkg">unpkg (npm package, pinned 3.32.0)</option>
</select>

<div class="three">
  <div><label for="lng">Longitude</label><input id="lng" value="30" inputmode="decimal"></div>
  <div><label for="lat">Latitude</label><input id="lat" value="15" inputmode="decimal"></div>
  <div><label for="zoom">Zoom</label><input id="zoom" value="1" inputmode="decimal"></div>
</div>

<textarea id="code" readonly aria-label="Generated page"></textarea>
<button id="copy" type="button">Copy the page</button>
<span id="msg" style="font-size:13px;margin-left:8px"></span>

<script>
  const $ = (id) => document.getElementById(id);

  function build() {
    const token = $('tok').value.trim();
    const w = $('warn');
    if (token.startsWith('sk.')) {
      w.innerHTML = '<span class="bad">That is a secret token (sk.). Never put it in a web page. Use a public pk. token.</span>';
    } else if (token && !token.startsWith('pk.')) {
      w.innerHTML = '<span class="bad">Public tokens start with pk.</span>';
    } else {
      w.innerHTML = token ? '<span class="ok">Looks like a public token.</span>' : '';
    }
    const safeToken = token.startsWith('sk.') ? 'YOUR_PUBLIC_TOKEN' : (token || 'YOUR_PUBLIC_TOKEN');
    const base = $('cdn').value === 'unpkg'
      ? 'https://unpkg.com/mapbox-gl@3.32.0/dist/'
      : 'https://api.mapbox.com/mapbox-gl-js/v3.32.0/';
    const lng = parseFloat($('lng').value) || 0;
    const lat = parseFloat($('lat').value) || 0;
    const zoom = parseFloat($('zoom').value) || 0;

    $('code').value = [
      '<!DOCTYPE html>',
      '<html>',
      '<head>',
      '<meta charset="utf-8">',
      '<meta name="viewport" content="width=device-width, initial-scale=1">',
      '<link href="' + base + 'mapbox-gl.css" rel="stylesheet">',
      '<script src="' + base + 'mapbox-gl.js"><\/script>',
      '<style>',
      '  body { margin: 0; padding: 0; }',
      '  #map { position: absolute; top: 0; bottom: 0; width: 100%; }',
      '</style>',
      '</head>',
      '<body>',
      '<div id="map"></div>',
      '<script>',
      '  const map = new mapboxgl.Map({',
      '    accessToken: ' + JSON.stringify(safeToken) + ',',
      "    container: 'map',",
      "    style: 'mapbox://styles/mapbox/standard',",
      '    center: [' + lng + ', ' + lat + '],  // [longitude, latitude]',
      '    zoom: ' + zoom,
      '  });',
      '  new mapboxgl.Marker().setLngLat([' + lng + ', ' + lat + ']).addTo(map);',
      '<\/script>',
      '</body>',
      '</html>'
    ].join('\n');
  }

  ['tok', 'lng', 'lat', 'zoom'].forEach((id) => $(id).addEventListener('input', build));
  $('cdn').addEventListener('change', build);

  $('copy').addEventListener('click', () => {
    const area = $('code');
    area.select();
    let done = false;
    try { done = document.execCommand('copy'); } catch (e) {}
    $('msg').textContent = done ? 'Copied.' : 'Selected. Press Ctrl+C (or long-press, Copy).';
  });

  build();
</script>
</body>
</html>
Type your public token, coordinates and zoom. The complete single-file page appears below, ready to copy.

Where the map draws, and where it does not

The map is drawn in the browser, but its styles and tiles come from Mapbox's servers with your token attached. That is a request from your page to another site.

On your own web page the requests go through. In a NOS shared page requests to other sites are blocked.
On your own web page the requests go through. In a NOS shared page requests to other sites are blocked.

NOS runs the page and its scripts, and the scripts can come from unpkg. NOS blocks fetch to other sites, so a Mapbox-hosted style will not draw its tiles in a shared page.

Use a NOS link for what runs inside the page: the code, the coordinate checks, the layout.

For a map that needs no outside request, see map markers on an SVG map. To show a place with only a link, see HTML link to a map location.

When it does not work

What you see Cause Fix
"A valid Mapbox access token is required" No token, or a token the service rejects Set accessToken to your public token
Blank area, no map The container has no height Give the div a height, or use the sizing from the example
mapboxgl is not defined The script tag is missing, wrong, or placed after your code Load the library first. See JavaScript not working
"Invalid LngLat latitude value" Latitude and longitude are swapped Write [longitude, latitude]
Works on your laptop, fails once published The token has URL restrictions that omit the new address Add the page address to the token's URL list
A sk. token in the page A secret token was pasted Use a public pk. token
Map stays empty in a NOS shared page Requests to other sites are blocked Test the live map on your own site

A map page is easier to show than to describe, and the people you send it to can drag and zoom it themselves. Sending the code as an attachment does not let them do that.

To share the page, paste it into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can try it themselves. Anyone with the link can open it without an account. If you change the code later, the same link shows the new version.

Remember the limit from above. A shared NOS page cannot reach Mapbox's servers, so use it for the parts that run in the page, and open the finished map from your own site.

Questions people ask

Does Mapbox GL JS need an API key?

It needs an access token. Mapbox says the token associates your map with a Mapbox account. Create one in your account, pass it as the accessToken option of the Map, and use a public token that starts with pk.

Is it safe to put the token in the HTML?

A public token (pk.) is designed for client-side code, so it can sit in the page. Mapbox lets you add URL restrictions so the token only works from the addresses you list. A secret token (sk.) must never be in a web page.

Is Mapbox GL JS free?

The Mapbox pricing page lists up to 50,000 map loads a month for Mapbox GL JS. A map load is counted when a Map object is initialized on a page. Check the pricing page for the current terms before you publish. Version 2 and later is licensed under the Mapbox Terms of Service and needs an active Mapbox account.

What is the difference between Mapbox GL JS and the Mapbox APIs?

Mapbox GL JS is the browser library that draws the map. The Mapbox web services, such as the Geocoding and Directions APIs, are separate HTTP endpoints. Each request to them carries an access_token query parameter. This page is about the library.

Will the map draw on a NOS share page?

The page and its scripts run, and unpkg is one of the allowed script hosts. NOS blocks requests to other sites, and the Mapbox styles and tiles come from Mapbox's servers, so expect the map area to stay empty there. Test the live map on your own site.

Keep reading