Map Legend
@maptoolkit/maplibre-legend-control
is a MapLibre GL JS control that shows a legend for the current map view: one row for each
kind of feature that is on screen, drawn the way the map draws it. Named places, peaks and
waters appear by name, in the map’s own font. The legend needs no configuration with the
standard styles: everything it shows comes from metadata inside the style
JSON, and that is also where you change it.
Add the legend
npm install @maptoolkit/maplibre-legend-control maplibre-glimport * as maplibregl from "maplibre-gl";
import { LegendControl } from "@maptoolkit/maplibre-legend-control";
import "@maptoolkit/maplibre-legend-control/style.css";
const map = new maplibregl.Map({ container: "map", style, center, zoom });
map.addControl(new LegendControl({ language: "en" }));MapLibre GL JS 6 is an ES module without a default export, so the namespace import above is the one that works. The control needs MapLibre GL JS 6.0.0 or later.
The control adds a button that opens the panel, by default in the bottom-left corner
above the Maptoolkit logo (add it after the logo control); pass a position to
addControl to move it. The panel opens above the button in the bottom corners and
below it in the top corners, and the ✕ at its top right or Escape closes it. It updates whenever the map comes to rest, and it lists
only what is inside the view.
If your map uses the Maptoolkit style switcher, the legend can live in its panel instead of a button of its own. Add the style switcher first, then pass it to the legend:
import { StyleControl } from "@maptoolkit/maplibre-style-control";
const styleControl = new StyleControl();
map.addControl(styleControl);
map.addControl(new LegendControl({ button: "style-control", styleControl }));The style switcher’s panel then ends with a “Legend” row. It opens the legend where the styles were, and a click on the style tile brings the styles back.
With button: "attribution" the word “Legend” stands in bold at the start of the map’s
attribution line while the attribution is expanded. The legend opens above it.
The map below shows all four options: "icon" at the top left, "style-control" in the
style switcher at the bottom left, the default "icon-text" at the bottom right, and
"attribution" on the attribution line.
How a legend is built
The legend uses two pieces of metadata in the style JSON. Both sit under the key
maptoolkit:legend, one on each layer and one at the root of the style. You edit them in
the Expert Mode of MapMaker, see Customize a Map Style.
Your own style. The control works with any MapLibre style. Maptoolkit styles come
with the metadata built in; in a style of your own, you write it by hand. Only the layer
tags are required. Without a manifest, rows and groups are labeled with a readable form of
their keys, so nature:vineyard becomes “Vineyard”. The fields of a tag are listed in
Add your own layers.
Layer tags tell the control what a layer contributes. Each layer that should appear
carries a tag in its own metadata. The tag names a group (a section of the legend,
such as roads or water) and a key (which row of that group the layer’s features belong
to). Together they form the row key group:key, for example road:major_dark or
nature:wood. Several layers can share a row key: the highway’s fill, its casing and its
blur are three layers that draw one thing, so the legend stacks them into one swatch.
The manifest at the root of the style names and orders what the tags produce. It has two parts:
groups: one entry per group, with its label and position.entries: one entry per row key, with its label, position, an optional link, and whether the row is hidden.
{
"layers": [
{
"id": "road_major_dark",
"type": "line",
"metadata": { "maptoolkit:legend": { "role": "main", "group": "road", "key": "major_dark" } }
}
],
"metadata": {
"maptoolkit:legend": {
"groups": { "road": { "label": { "en": "Roads and transport", "de": "Straßen und Verkehr" }, "order": 20 } },
"entries": { "road:major_dark": { "label": { "en": "Highway", "de": "Autobahn" }, "order": 71 } }
}
}
}In short: tags decide which rows exist and what they show, the manifest decides what they are called and in which order they come. To change a label, hide a row or link a document, edit the manifest. To put a layer of your own into the legend, give it a tag.
A row appears only while a feature of it is in view. The row’s label comes from the manifest in the language you passed to the control; a missing language falls back to English, and a missing label to a readable form of the key.
Configure the rows
Edit the manifest entry of a row to rename, hide, reorder or link it. Fields you do not
set keep the style’s defaults, so a change is one small entry, not a copy of the whole
manifest. Groups take the same label, order and hidden fields, and sort.
"metadata": {
"maptoolkit:legend": {
"groups": {
"poi": { "hidden": true }
},
"entries": {
"road:major_dark": { "label": { "en": "Motorway" } },
"road:minor_track": { "hidden": true },
"water:waterway": { "order": 1 },
"road:path_sac_scale_label": {
"link": "https://www.sac-cas.ch/fileadmin/Ausbildung_und_Sicherheit/Tourenplanung/Alpinmerkbl%C3%A4tter/20230601_SAC-Wanderskala_D.pdf"
},
"place:village": { "keys": ["place:hamlet", "place:isolated_dwelling", "place:farm"] }
}
}
}| Field | Type | Description |
|---|---|---|
label | object | The text of the row or group, one string per language code, for example { "en": "Motorway", "de": "Autobahn" }. |
hidden | boolean | Optional. true removes the row, or the whole group, from the legend. |
order | number | Optional. Rows within a group, and groups, sort by order, lowest first, then by label. |
link | string or object | Optional, rows only. A URL that explains the row, shown as a small ⓘ button after the label. One URL, or one per language like label. |
sort | string | Optional, groups only. "rank" orders the group’s rows by the rank of the feature each shows, the most prominent first across all types, instead of by order. The standard styles sort the points of interest this way. |
keys | string array | Optional, rows only. Other row keys this row stands for: their features are listed under this row instead of their own. The standard styles list hamlets and farms under the village row this way. |
To find a row’s key, open the style JSON in MapMaker’s Expert Mode and search the
manifest’s entries for the row’s label text; the key is the name of that entry. For a
row of your own layer, the key is group:key from the layer’s tag.
Add your own layers
A layer of yours appears in the legend once it carries a tag: an object under
metadata["maptoolkit:legend"] on the layer itself. A fill layer for vineyards with a
tag looks like this; the row it produces is nature:vineyard, labelled by the manifest
entry of the same name.
{
"id": "vineyards",
"type": "fill",
"source": "farms",
"paint": { "fill-color": "#c9a7d9" },
"metadata": {
"maptoolkit:legend": { "role": "main", "group": "nature", "key": "vineyard" }
}
}"entries": { "nature:vineyard": { "label": { "en": "Vineyard", "de": "Weingarten" } } }The fields below are read from that tag. Start with the three every layer needs, add the common ones when your layer is more than a single fill or line, and keep the special ones for the cases they name.
The fields every tagged layer needs
| Field | Meaning |
|---|---|
role | What the layer contributes. main means the layer is a row of its own and its fill or stroke is the swatch. Every other role decorates another layer’s row: casing, blur, shadow, base, outline, hatching, texture and band are drawn into that row’s swatch, label, shield and symbol are map symbols (see instance). |
group | The section of the legend the row belongs to: road, water, nature, border, building, relief, place or poi, or a name of your own. |
key | The row within the group. With group it forms the row key group:key, which is also the name of the manifest entry that labels the row. Use keyProperty instead when one layer should form several rows in the legend. |
A main layer with group and key is complete. A decorating layer needs attachesTo
instead of key; a symbol layer needs instance.
Common fields
| Field | Meaning |
|---|---|
hidden | true keeps the layer out of the legend. For helper layers such as a mask or a glow. No other field is needed then. |
attachesTo | For decorating roles: the ids of the main layers whose row this layer belongs to, for example a casing under a road. Its stroke is stacked into that row’s swatch. The row itself comes from the main layer. |
keyProperty | Instead of key: the name of a feature property whose value is the key. A land-use layer with keyProperty: "type" gives the rows nature:wood, nature:grass and so on, one for every value that is on screen. Label each value in the manifest. |
instance | true on a label, shield or symbol layer: the row shows a real symbol from the map, such as a place name in the map font or a route shield, instead of a swatch. With keyProperty the layer gives one row per value, each showing the most prominent feature of that value; with key it gives one row for the layer. |
rankProperty | With instance and keyProperty: the feature property that ranks the candidates; the lowest value is the one shown. The Maptoolkit labels use rank, the POI labels rank_new. |
Special fields
| Field | Meaning |
|---|---|
anchors | On an instance layer: the ids of the main layers the symbol sits on. The row then draws the symbol on that layer’s line or surface, like a shield on its route, whenever that layer is in view. |
keyByValue | On a main layer that paints a few property values differently: [{ "property": "subtype", "values": { "pedestrian": "minor_pedestrian" } }] sends features with that value to another row while the rest keeps the layer’s key. "*" as a value stands for any value the property has. |
overlay | true on a main layer drawn on top of other lines, such as a hiking route on a path. The swatch then shows the route together with the line under it. |
crossing | "bridge" or "tunnel" on a duplicate layer that draws the same features on bridges or in tunnels. The legend prefers the ground-level copy for the swatch. |
Control options
| Option | Default | Description |
|---|---|---|
language | page language | Language of the labels. |
groups | all | Show only these groups, for example ["road", "place"]. |
collapsed | true | Start with the panel hidden. The panel starts closed with every button; with toggle: false it starts open. |
toggle | true | Show the button that opens and closes the panel. Set false when your page has its own trigger and calls open() and close(). |
button | "icon-text" | What the button shows: a small legend-row icon and the word “Legend” in the control’s language, or "icon" for the icon alone. "style-control" adds the legend to the style switcher’s panel instead of a button of its own, "attribution" puts the word “Legend” in front of the map’s attribution. |
styleControl | – | The style switcher (StyleControl from @maptoolkit/maplibre-style-control) that hosts the legend with button: "style-control". Add it to the map before the legend. |
maxNameWidth | { fraction: 0.5, px: 260 } | How wide a name in the map font may grow before it is cut. false removes the limit. |
minOpacity | 0.1 | Rows whose layers are all fainter than this, for example a fill that is fading in between zooms, are left out. 0 shows every row. |
background | "auto" | Panel background: the style’s background color at the current zoom, or a CSS color. |
fonts | from the style | URL of the web font stylesheet. false if your page provides the fonts. |
The control’s own texts — the button’s word, its tooltip, the empty message, the ⓘ
tooltip and the ✕ tooltip — come in English, German, Spanish, Italian, French, Hungarian, Czech, Polish,
Chinese, Japanese, Korean, Hindi and Arabic, chosen by language. To change one, set it
in the map’s locale table, like for MapLibre’s built-in controls, for example
locale: { "LegendControl.Label": "Key" }. The keys are LegendControl.Label,
LegendControl.Title, LegendControl.Toggle, LegendControl.Empty,
LegendControl.Info and LegendControl.Close.
Fonts for names
The legend sets names in the map’s typeface. It links the web font stylesheet named in
the manifest under fonts.css; Maptoolkit styles point it to the fonts of their map
labels. If your style uses other fonts, name your own stylesheet there, or pass it as the
fonts option, which takes precedence. Without a matching font, names show in a fallback
font.
"metadata": { "maptoolkit:legend": { "fonts": { "css": "https://example.com/fonts.css" } } }The stylesheet needs an @font-face for each font your labels use. Its font-family is
the first text-font name without the weight and style words, so
"Roboto Condensed Bold Italic" becomes:
@font-face {
font-family: "Roboto Condensed";
font-weight: 700;
font-style: italic;
src: url("roboto-condensed-700-italic.woff2") format("woff2");
}Example workflows
Each example states where the change goes. The manifest is the object under
metadata["maptoolkit:legend"] at the root of the style; a layer tag is the object of
the same name inside the metadata of a single layer. You make both kinds of change in MapMaker: its Expert Mode
gives you the style JSON, where a layer’s metadata and the style’s root metadata are
edited like any other part of the style.
Rename a row. This is a manifest change only. Search the manifest’s entries in MapMaker for the row’s current label to find its key, then set the label for your language. The labels of the other
languages keep their default text.
"entries": { "road:major_dark": { "label": { "en": "Motorway" } } }Hide rows you do not need. This is also a manifest change only. You can hide a single row, or a whole group with all its rows at once.
"groups": { "poi": { "hidden": true } },
"entries": { "road:minor_track": { "hidden": true }, "road:minor_oneway_symbol": { "hidden": true } }Put the important rows first. In the manifest, give the rows that matter most a low
order value. They move to the top of their group, and all other rows keep their
relative position.
"entries": { "road:hiking": { "order": 1 }, "road:path_alpine": { "order": 2 } }Show only some groups. This needs no change to the style at all. Pass the groups you want to the control, and it leaves out every other group.
map.addControl(new LegendControl({ language: "en", groups: ["road", "place"] }));Add a fill layer of your own. This takes a layer tag and a manifest entry, as in the
vineyard example above. The tag on the layer creates the row nature:vineyard, and the
manifest entry with the same key gives the row its label.
A line with a casing as one row. This takes two layer tags and one manifest entry.
The casing layer names the main layer in its attachesTo field, so the legend draws both
layers into the single row road:minor, with the casing under the line as on the map.
{ "id": "minor_casing", "type": "line", "metadata": { "maptoolkit:legend": { "role": "casing", "group": "road", "attachesTo": ["minor"] } } },
{ "id": "minor", "type": "line", "metadata": { "maptoolkit:legend": { "role": "main", "group": "road", "key": "minor" } } }"entries": { "road:minor": { "label": { "en": "Minor roads" }, "order": 1 } }One layer, one row per value. This takes a layer tag with keyProperty and one
manifest entry per value. A layer that assigns dedicated colors to parks depending on their
subtype produces the rows nature:park, nature:playground and so on, which you then
label in the manifest.
{ "id": "parks", "type": "fill", "metadata": { "maptoolkit:legend": { "role": "main", "group": "nature", "keyProperty": "subtype" } } }"entries": {
"nature:park": { "label": { "en": "Park" } },
"nature:playground": { "label": { "en": "Playground" } }
}Names in the map font. This requires a layer tag with instance and one manifest entry
per legend item. A symbol layer that labels your points of interest lists one named feature for
each type value and picks the feature with the lowest rank value for it. The
name appears in the font the map uses for the label.
{
"id": "shop_labels",
"type": "symbol",
"metadata": { "maptoolkit:legend": { "role": "label", "group": "poi", "instance": true, "keyProperty": "type", "rankProperty": "rank" } }
}"entries": { "poi:bakery": { "label": { "en": "Bakery" } }, "poi:cafe": { "label": { "en": "Café" } } }A shield on its route. This takes a layer tag with instance, key and anchors,
and one manifest entry. The shield gets a row of its own, and while a trail is in view the
legend draws the shield on the trail’s line, the way the map does.
{
"id": "trail_shields",
"type": "symbol",
"metadata": { "maptoolkit:legend": { "role": "shield", "group": "road", "instance": true, "key": "trail_shield", "anchors": ["trails"] } }
}"entries": { "road:trail_shield": { "label": { "en": "Trail number" }, "link": "https://example.org/trail-grades.pdf" } }Keep a helper layer out. This takes a layer tag only. A layer tagged as hidden never appears in the legend, which is what you want for a glow, a mask or any other layer that only supports the look of another one.
{ "id": "trails_glow", "type": "line", "metadata": { "maptoolkit:legend": { "hidden": true } } }One layer, two looks: a row for the exception. This takes a layer tag with
keyByValue and one extra manifest entry. Sometimes a single layer paints a few of its
features differently, for example a street layer that draws pedestrian zones in another
color than the rest. Without help, all of its features land in one row, and the legend
shows only one of the two looks. keyByValue names the property and the values that get a
row of their own; every other feature stays in the layer’s normal row. The layer below
gives the rows road:street and road:street_pedestrian. The value "*" stands for any
value at all, which is useful when the property is only present on the exceptions.
{
"id": "streets",
"type": "line",
"metadata": {
"maptoolkit:legend": {
"role": "main",
"group": "road",
"key": "street",
"keyByValue": [{ "property": "subtype", "values": { "pedestrian": "street_pedestrian" } }]
}
}
}"entries": {
"road:street": { "label": { "en": "Street" } },
"road:street_pedestrian": { "label": { "en": "Pedestrian zone" } }
}A route drawn on top of roads. This takes a layer tag with overlay and one manifest
entry. A hiking route is a band painted over the paths and roads it follows, so on its own
the band says little. With overlay set, the legend shows the row with the whole stack
the map draws at that place: the route band together with the road under it. When the
route runs over several kinds of road in the current view, the legend picks a stretch on a
plain road over one on a bridge, and one without a second route on it, so the swatch
shows the route as clearly as possible.
{
"id": "hiking_route",
"type": "line",
"metadata": { "maptoolkit:legend": { "role": "main", "group": "road", "key": "hiking_route", "overlay": true } }
}"entries": { "road:hiking_route": { "label": { "en": "Hiking route" } } }The same road on a bridge or in a tunnel. This takes layer tags with crossing and no
extra manifest entry. Many styles draw a road twice: one layer for the ordinary stretches
and a second layer, with the same look plus a shadow or a dashed casing, for the stretches
on bridges or in tunnels. Both layers belong to the same row, so the second one gets the
same key and, in addition, crossing set to "bridge" or "tunnel". The legend then
knows that the two layers draw the same road and prefers the ordinary layer for the
swatch, so a bridge shadow does not end up under every road in the legend. Only when a
road is in view on a bridge alone does the bridge layer stand in. A supporting layer that
exists only for the crossing, such as the shadow below, carries crossing as well.
{ "id": "streets", "type": "line", "metadata": { "maptoolkit:legend": { "role": "main", "group": "road", "key": "street" } } },
{ "id": "streets_bridge", "type": "line", "metadata": { "maptoolkit:legend": { "role": "main", "group": "road", "key": "street", "crossing": "bridge" } } },
{ "id": "streets_bridge_shadow", "type": "line", "metadata": { "maptoolkit:legend": { "role": "shadow", "group": "road", "attachesTo": ["streets_bridge"], "crossing": "bridge" } } }"entries": { "road:street": { "label": { "en": "Street" } } }