Skip to content
Go back

Label Callouts for MapLibre

Published:  at  11:00 PM

The problem

MapLibre places labels next to their features. When features are close together, labels overlap and get hidden. The built in collision detection stops overlap but does not move labels elsewhere or connect them back to their feature.

Callouts solve this. The label sits in open space where text is readable. A connector line ties the label to its feature. Multiple callouts stack side by side without overlapping.

The idea came from ESRI Callout Line 3D, a feature in the ArcGIS API for JavaScript. I wanted the same thing for MapLibre.

MapLibre map showing port labels with colored connector lines and pastel callout boxes

How it works

The plugin only looks at features that are currently visible on screen. When you pan or zoom, it asks MapLibre “what features can the user see right now?” and ignores everything else. This is viewport culling.

While you drag, rotate, or tilt the map, the plugin does almost nothing. It just moves the existing labels to their new screen positions with map.project(). No new queries, no collision checks, no DOM creation. That heavy work only happens when you stop moving the map. And even then it runs at most once per frame, scheduled with requestAnimationFrame.

The collision system is simple greedy. Features are sorted by priority (for example, number of ships in a port). The highest priority labels are placed first. Once a spot is taken, the next label tries the next best position. If there is no room, the label is skipped and the feature stays as a plain dot on the map.

DOM elements (the label divs) are created once and reused. When a label is no longer needed, its div is hidden instead of removed. This keeps the DOM stable.

There are also zoom and pitch limits. If the map is too far zoomed out or too tilted, labels are hidden and nothing is processed.

Per feature styling

Each callout can be styled individually through callback functions. Every feature gets its own colors for background, text, border, connector line, and marker dot. A hash function maps each feature ID to a pastel color scheme, so the same feature always gets the same colors.

lineColor: (p) => p.c_line,
labelBackground: (p) => p.c_bg,
labelColor: (p) => p.c_text,
labelBorder: (p) => "1px solid " + p.c_border,

How to use

Install with npm:

npm install @leoneljdias/maplibre-label-callout

Or load from a script tag:

<script src="https://unpkg.com/@leoneljdias/maplibre-label-callout"></script>

Basic usage:

import LineCallout3D from "@leoneljdias/maplibre-label-callout";

const callouts = new LineCallout3D(map, {
  data: myGeoJSON,
  idProperty: "id",
  template: (p) => `<div>${p.name}</div>`,
  maxLabels: 25,
});

Bundle size

The plugin is about 3KB minified. No dependencies beyond MapLibre GL JS. Connector lines and markers use MapLibre’s built in layer types (line, circle), so there are no custom shaders or extra rendering overhead.



Next Post
Map Patterning: Assigning SVG Textures to Hundreds of Adjacent Polygons