Skip to content
Map Legend

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-gl
import * 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"] }
    }
  }
}
FieldTypeDescription
labelobjectThe text of the row or group, one string per language code, for example { "en": "Motorway", "de": "Autobahn" }.
hiddenbooleanOptional. true removes the row, or the whole group, from the legend.
ordernumberOptional. Rows within a group, and groups, sort by order, lowest first, then by label.
linkstring or objectOptional, rows only. A URL that explains the row, shown as a small ⓘ button after the label. One URL, or one per language like label.
sortstringOptional, 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.
keysstring arrayOptional, 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

FieldMeaning
roleWhat 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).
groupThe section of the legend the row belongs to: road, water, nature, border, building, relief, place or poi, or a name of your own.
keyThe 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

FieldMeaning
hiddentrue keeps the layer out of the legend. For helper layers such as a mask or a glow. No other field is needed then.
attachesToFor 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.
keyPropertyInstead 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.
instancetrue 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.
rankPropertyWith 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

FieldMeaning
anchorsOn 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.
keyByValueOn 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.
overlaytrue 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

OptionDefaultDescription
languagepage languageLanguage of the labels.
groupsallShow only these groups, for example ["road", "place"].
collapsedtrueStart with the panel hidden. The panel starts closed with every button; with toggle: false it starts open.
toggletrueShow 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.
minOpacity0.1Rows 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.
fontsfrom the styleURL 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" } } }