Show Temperature, Rain and Wind in MapLibre GL JS
On this page
The Weather API delivers temperature, rain and wind as vector tiles. This example shows all three on a MapLibre GL JS map, below the place names, with buttons to switch between them and a legend in the colors of the Maptoolkit weather map.
import * as maplibregl from "maplibre-gl";
import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
const API_KEY = "YOUR_API_KEY";
// Colors and opacities of the Maptoolkit weather map at maptoolkit.com/weather.
const LAYERS = {
temperature: {
field: "degree", unit: "°C", opacity: 0.4, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.2)", type: "interpolate",
stops: [[-30, "rgb(150, 0, 104)"], [-20, "rgb(0, 0, 120)"], [-10, "rgb(0, 138, 196)"], [0, "rgb(20, 165, 145)"],
[10, "rgb(140, 200, 30)"], [20, "rgb(255, 219, 0)"], [30, "rgb(210, 100, 10)"], [40, "rgb(135, 0, 15)"]],
},
precipitation: {
field: "mm", unit: "mm", opacity: 0.4, legendOpacity: 0.45, outline: "rgba(0, 0, 0, 0.225)", type: "interpolate",
stops: [[0.2, "rgb(47, 75, 190)"], [1, "rgb(21, 122, 151)"], [2, "rgb(10, 165, 77)"], [5, "rgb(0, 210, 0)"],
[12, "rgb(255, 255, 0)"], [20, "rgb(230, 100, 0)"], [25, "rgb(229, 0, 0)"], [30, "rgb(170, 0, 29)"]],
},
wind: {
// The tiles carry m/s; the legend shows km/h like the Maptoolkit weather map.
field: "ms", unit: "km/h", opacity: 0.35, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.175)", type: "step", toLabel: (ms) => Math.round(ms * 3.6),
base: "rgb(100, 0, 254)",
stops: [[4, "rgb(0, 50, 254)"], [6, "rgb(0, 150, 254)"], [8, "rgb(0, 230, 240)"], [10, "rgb(17, 212, 17)"],
[12, "rgb(0, 250, 0)"], [14, "rgb(254, 254, 0)"], [16, "rgb(254, 200, 0)"], [18, "rgb(254, 150, 0)"],
[20, "rgb(230, 100, 0)"], [22, "rgb(200, 50, 29)"], [24, "rgb(170, 0, 29)"], [26, "rgb(200, 0, 100)"], [28, "rgb(254, 0, 150)"]],
},
};
function fillColor({ type, field, stops, base }) {
return type === "step"
? ["step", ["get", field], base, ...stops.flat()]
: ["interpolate", ["linear"], ["get", field], ...stops.flat()];
}
// One legend row per stop, highest first, tinted with the layer's own color and opacity.
function legendRows({ stops, base, legendOpacity, toLabel = (v) => v }) {
const rows = stops.map(([value, color]) => [toLabel(value), color]);
if (base) rows.unshift([toLabel(stops[0][0] / 2), base]);
return rows.reverse().map(([label, color]) =>
`<div style="background:${color.replace("rgb(", "rgba(").replace(")", `, ${legendOpacity})`)}">${label}</div>`).join("");
}
const map = new maplibregl.Map({
container: "map",
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [11, 48.5],
zoom: 5,
// The weather tiles start at zoom 3; below that there is nothing to show.
minZoom: 3,
attributionControl: { compact: false },
});
map.addControl(new maplibregl.NavigationControl(), "top-right");
function show(key) {
for (const name of Object.keys(LAYERS)) {
map.setLayoutProperty(`weather-${name}`, "visibility", name === key ? "visible" : "none");
}
document.querySelectorAll("#weather button").forEach((button) =>
button.classList.toggle("active", button.dataset.key === key));
document.getElementById("legend").innerHTML = `<div class="unit">${LAYERS[key].unit}</div>` + legendRows(LAYERS[key]);
}
// "style.load" fires as soon as the style is parsed; "load" would also wait for every tile.
map.once("style.load", () => {
map.addSource("weather", { type: "vector", url: `https://weather.maptoolkit.net/vector/icon.0.json?api_key=${API_KEY}` });
// Below the first label layer, so place names stay readable on top of the weather.
const firstSymbolId = map.getStyle().layers.find((layer) => layer.type === "symbol")?.id;
for (const [name, layer] of Object.entries(LAYERS)) {
map.addLayer({
id: `weather-${name}`, type: "fill", source: "weather", "source-layer": name,
layout: { visibility: "none" },
paint: { "fill-color": fillColor(layer), "fill-opacity": layer.opacity, "fill-outline-color": layer.outline },
}, firstSymbolId);
}
show("temperature");
});
document.querySelectorAll("#weather button").forEach((button) =>
button.addEventListener("click", () => show(button.dataset.key)));
map.addControl(new LogoControl({ variant: "com" }));<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.16.0/dist/maplibre-gl.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.css" />
<!-- MapLibre GL JS 6 is an ES module. The import map tells the browser
where to load "maplibre-gl" from. -->
<script type="importmap">
{
"imports": {
"maplibre-gl": "https://cdn.jsdelivr.net/npm/maplibre-gl@6.16.0/dist/maplibre-gl.mjs"
}
}
</script>
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
#weather { position: absolute; top: 10px; left: 10px; z-index: 1; width: 36px; font: 11px/1 system-ui, sans-serif; }
#weather .buttons, #weather .legend { background: #fff; border-radius: 6px; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); overflow: hidden; }
#weather button {
display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: none; background: none;
cursor: pointer; color: #9aa0b4;
}
#weather button + button { border-top: 1px solid #eee; }
#weather button.active { color: #303f7e; background: #eef0f6; }
#weather button svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
#weather .legend { margin-top: 10px; text-align: center; }
#weather .unit { padding: 5px 0; font-weight: 600; }
#weather .legend div:not(.unit) { padding: 5px 0; color: #1f2430; }
</style>
</head>
<body>
<div id="map"></div>
<div id="weather">
<div class="buttons">
<button data-key="temperature" title="Temperature"><svg viewBox="0 0 24 24"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"/></svg></button>
<button data-key="precipitation" title="Precipitation"><svg viewBox="0 0 24 24"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/></svg></button>
<button data-key="wind" title="Wind"><svg viewBox="0 0 24 24"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"/></svg></button>
</div>
<div class="legend" id="legend"></div>
</div>
<script type="module">
import * as maplibregl from "maplibre-gl";
import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
const API_KEY = "YOUR_API_KEY";
// Colors and opacities of the Maptoolkit weather map at maptoolkit.com/weather.
const LAYERS = {
temperature: {
field: "degree", unit: "°C", opacity: 0.4, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.2)", type: "interpolate",
stops: [[-30, "rgb(150, 0, 104)"], [-20, "rgb(0, 0, 120)"], [-10, "rgb(0, 138, 196)"], [0, "rgb(20, 165, 145)"],
[10, "rgb(140, 200, 30)"], [20, "rgb(255, 219, 0)"], [30, "rgb(210, 100, 10)"], [40, "rgb(135, 0, 15)"]],
},
precipitation: {
field: "mm", unit: "mm", opacity: 0.4, legendOpacity: 0.45, outline: "rgba(0, 0, 0, 0.225)", type: "interpolate",
stops: [[0.2, "rgb(47, 75, 190)"], [1, "rgb(21, 122, 151)"], [2, "rgb(10, 165, 77)"], [5, "rgb(0, 210, 0)"],
[12, "rgb(255, 255, 0)"], [20, "rgb(230, 100, 0)"], [25, "rgb(229, 0, 0)"], [30, "rgb(170, 0, 29)"]],
},
wind: {
// The tiles carry m/s; the legend shows km/h like the Maptoolkit weather map.
field: "ms", unit: "km/h", opacity: 0.35, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.175)", type: "step", toLabel: (ms) => Math.round(ms * 3.6),
base: "rgb(100, 0, 254)",
stops: [[4, "rgb(0, 50, 254)"], [6, "rgb(0, 150, 254)"], [8, "rgb(0, 230, 240)"], [10, "rgb(17, 212, 17)"],
[12, "rgb(0, 250, 0)"], [14, "rgb(254, 254, 0)"], [16, "rgb(254, 200, 0)"], [18, "rgb(254, 150, 0)"],
[20, "rgb(230, 100, 0)"], [22, "rgb(200, 50, 29)"], [24, "rgb(170, 0, 29)"], [26, "rgb(200, 0, 100)"], [28, "rgb(254, 0, 150)"]],
},
};
function fillColor({ type, field, stops, base }) {
return type === "step"
? ["step", ["get", field], base, ...stops.flat()]
: ["interpolate", ["linear"], ["get", field], ...stops.flat()];
}
// One legend row per stop, highest first, tinted with the layer's own color and opacity.
function legendRows({ stops, base, legendOpacity, toLabel = (v) => v }) {
const rows = stops.map(([value, color]) => [toLabel(value), color]);
if (base) rows.unshift([toLabel(stops[0][0] / 2), base]);
return rows.reverse().map(([label, color]) =>
`<div style="background:${color.replace("rgb(", "rgba(").replace(")", `, ${legendOpacity})`)}">${label}</div>`).join("");
}
const map = new maplibregl.Map({
container: "map",
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [11, 48.5],
zoom: 5,
// The weather tiles start at zoom 3; below that there is nothing to show.
minZoom: 3,
attributionControl: { compact: false },
});
map.addControl(new maplibregl.NavigationControl(), "top-right");
function show(key) {
for (const name of Object.keys(LAYERS)) {
map.setLayoutProperty(`weather-${name}`, "visibility", name === key ? "visible" : "none");
}
document.querySelectorAll("#weather button").forEach((button) =>
button.classList.toggle("active", button.dataset.key === key));
document.getElementById("legend").innerHTML = `<div class="unit">${LAYERS[key].unit}</div>` + legendRows(LAYERS[key]);
}
// "style.load" fires as soon as the style is parsed; "load" would also wait for every tile.
map.once("style.load", () => {
map.addSource("weather", { type: "vector", url: `https://weather.maptoolkit.net/vector/icon.0.json?api_key=${API_KEY}` });
// Below the first label layer, so place names stay readable on top of the weather.
const firstSymbolId = map.getStyle().layers.find((layer) => layer.type === "symbol")?.id;
for (const [name, layer] of Object.entries(LAYERS)) {
map.addLayer({
id: `weather-${name}`, type: "fill", source: "weather", "source-layer": name,
layout: { visibility: "none" },
paint: { "fill-color": fillColor(layer), "fill-opacity": layer.opacity, "fill-outline-color": layer.outline },
}, firstSymbolId);
}
show("temperature");
});
document.querySelectorAll("#weather button").forEach((button) =>
button.addEventListener("click", () => show(button.dataset.key)));
map.addControl(new LogoControl({ variant: "com" }));
</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
Weather is a vector tile source, not a REST call. One source, added with a TileJSON URL,
holds a layer per value: temperature with the field degree, precipitation with mm and
wind with ms. The map streams the tiles as you pan.
One fill layer per value, switched with visibility. All three layers are added once, with
their own source-layer, and the buttons on the left set visibility to show one of them. The
source and its tiles are shared, so switching does not download anything again.
The colors are those of the Maptoolkit weather map. Temperature and rain interpolate
between the same stops, at the same opacity, as maptoolkit.com/weather,
and wind uses its step expression in 2 m/s steps. A thin dark outline separates the areas. The
legend is built from the same stops, highest value on top, so a color on the map always has its
row. The wind tiles store m/s, and toLabel converts the legend to km/h, as on the weather map.
Below the labels. Passing the first symbol layer’s id as the second argument to addLayer
inserts the weather under it, so place names stay readable on top.
Zoom limits. The weather tiles cover zoom levels 3 to 8. minZoom: 3 stops the map there;
zoomed out further, the layer would disappear. Beyond zoom 8 the zoom 8 data is stretched.
The layers are added on style.load, which fires as soon as the style is ready. The more
common load event also waits for every initial tile of the basemap, which delays the weather
without need.
Coverage is not global: the ICON models cover Germany and its neighbors at high resolution and Europe more broadly. Outside that area the tiles are empty.
Next steps
The tiles also carry a weather_label layer with temperature, humidity, wind speed and
direction per place, which a symbol layer can print on the map.
To report the exact value under the cursor, query the visible layer on click with
queryRenderedFeatures() and show its field in a popup.