Switch Between Globe and Mercator in Maptoolkit Maps JS
Globe reads well zoomed out and stops earning its cost once the curvature is no longer visible. Maps JS already handles that hand-off for you, which is the part worth understanding before you override it. This example makes the projection explicit so you can compare the two, and shows the area distortion that is the real argument for switching.
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [13.0, 47.5],
zoom: 2,
// The style is authored from zoom 1 up, so stop the camera before it runs out of it.
minZoom: 1,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
// The control users actually get: one button that toggles the projection.
map.addControl(new maptoolkit.GlobeControl(), 'top-right');
function report() {
const zoom = map.getZoom();
const lat = map.getCenter().lat;
// Web Mercator inflates area by 1 / cos(latitude).
const inflation = 1 / Math.cos(lat * Math.PI / 180);
const projection = map.getProjection?.()?.type ?? 'mercator';
const showingGlobe = projection === 'globe' && zoom < 6;
document.getElementById('note').innerHTML =
`<b>${showingGlobe ? 'Globe' : 'Mercator'}</b>` +
`<span class="fact">zoom ${zoom.toFixed(1)}, ` +
`Mercator would inflate area here by ${inflation.toFixed(1)}x</span>`;
}
map.on('move', report);
map.on('load', report);<!DOCTYPE html>
<html lang="en">
<head>
<title>Globe and Mercator - Maptoolkit Maps JS</title>
<meta property="og:description" content="Switch the map projection between globe and Mercator." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
#note {
position: absolute; bottom: 34px; left: 10px; z-index: 999;
background: #fff; border-radius: 8px; box-shadow: 0 2px 16px #0003;
font: 13px/1.5 system-ui, sans-serif; padding: 11px 14px; max-width: 250px;
}
#note b { display: block; margin-bottom: 2px; }
#note .fact { color: #5b6170; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div id="map"></div>
<div id="note"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [13.0, 47.5],
zoom: 2,
// The style is authored from zoom 1 up, so stop the camera before it runs out of it.
minZoom: 1,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
// The control users actually get: one button that toggles the projection.
map.addControl(new maptoolkit.GlobeControl(), 'top-right');
function report() {
const zoom = map.getZoom();
const lat = map.getCenter().lat;
// Web Mercator inflates area by 1 / cos(latitude).
const inflation = 1 / Math.cos(lat * Math.PI / 180);
const projection = map.getProjection?.()?.type ?? 'mercator';
const showingGlobe = projection === 'globe' && zoom < 6;
document.getElementById('note').innerHTML =
`<b>${showingGlobe ? 'Globe' : 'Mercator'}</b>` +
`<span class="fact">zoom ${zoom.toFixed(1)}, ` +
`Mercator would inflate area here by ${inflation.toFixed(1)}x</span>`;
}
map.on('move', report);
map.on('load', report);
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
How it works
GlobeControl is the control to reach for. It ships with Maps JS, adds one button to the
map and toggles the projection, so this is a line of setup rather than an interface you
design. Building your own row of projection buttons is the thing to avoid: readers already
know what the globe button does, and a bespoke control has to be styled, positioned and kept
accessible for no gain.
Under the button, setProjection({ type: 'globe' }) does not mean “always draw a
sphere”. It is adaptive: the map renders as a globe while zoomed out and transitions to
Mercator as you zoom in, around zoom 6, because curvature stops being visible and a flat
projection is more accurate locally. So the genuine choice is between 'globe' and
'mercator', and picking 'mercator' is the one that changes behaviour, by pinning the flat
projection at world zoom too.
minZoom: 1 is not arbitrary, and it does not cover every case. Only 13 of the Summer
style’s 107 layers render below zoom 1, and none of them are label layers, so under zoom 1
the map falls back to coastlines, water, borders and the Natural Earth raster with no names on
it at all. The style is authored from zoom 1 up, and minZoom keeps the zoom control inside
that range.
Panning to a pole gets under it anyway, and no setting prevents that. Globe projection
compensates for the way Mercator stretches area towards the poles, so the effective zoom drops
as you travel north or south: half a level at 45 degrees, one level at 60, and 3.5 levels at
85, which is where MapLibre clamps latitude. Keeping the effective zoom at 1 there would mean
a minZoom of about 4.5, and at 4.5 you cannot see a globe at all. The two requirements are in
direct conflict.
So at the poles this map shows an unlabelled world, and that is the style’s floor rather than a bug in the code. If you need names on a globe at world zoom, the fix is in the style: its label layers have to start at zoom 0.
The area inflation readout is the reason any of this matters. Web Mercator scales area by
1 / cos(latitude), so Austria is drawn about 1.5 times its true area, Scandinavia three
times, and Greenland enough to look the size of Africa. Any map where a reader compares the
size of two regions at different latitudes is lying to them on Mercator, and that is the
strongest argument for globe on a world view.
Globe costs more to render, and the transition is where it shows. Starting a page on a globe means paying for the sphere before the reader has asked for a world view.
Not everything survives the switch. A custom layer written against Mercator coordinates, the kind a three.js scene uses, assumes a flat projection and will be positioned wrongly on the sphere. Test any custom layer at world zoom before shipping globe.
map.on('move') rather than moveend drives the readout, because the zoom number should
track the gesture rather than appear after it.
Next steps
A globe earns its cost when the data is global. Flight paths, shipping routes and a worldwide customer map all read correctly on a sphere and wrongly on Mercator, where a great-circle line that is actually straight appears bent.
An atmosphere and a star field are what make the sphere look deliberate rather than accidental, and they only read against the dark styles.