Popups¶
Popups are turned on per layer; a layer with no popup field has none. Once on, clicking (or hovering) a feature opens a popup drawn by a template in your project's popups.tsx.
Two blocks set them up:
ui.popupsinsettings.json: defaults for every layer (trigger, template, size, behavior).popupon a layer inlayers.json: settings for one layer (which fields, which template, a header).
They merge as kit default ← ui.popups ← layer.popup.
On a PMTiles layer that draws several source layers, layer.popup applies to every source layer unless a sourceLayers entry sets its own popup. Those popups' layer name reads <layer name> / <source layer name>, e.g. Protomaps / Roads, so you can tell which source layer a feature came from.
Global defaults: ui.popups¶
| Field | Type | Default | Description |
|---|---|---|---|
trigger |
"click" \| "hover" |
"click" |
Trigger for every layer that doesn't set its own |
position |
"bottom-left" and the other three corners, or "feature" |
"bottom-left" |
Where the desktop popup opens: a map corner, or next to the feature (below) |
displayType |
string |
"default" |
Template name from popups.tsx |
height |
number \| "auto" |
100 |
Popup height in px. A fixed height keeps the popup from jumping as you page through stacked features. "auto" fits the content, up to maxHeight |
maxHeight |
number |
400 |
Height limit when height is "auto" |
maxWidth |
number |
320 |
Desktop popup width in px. On mobile the sheet is full width |
draggable |
boolean |
true |
Let users drag the desktop popup by its header. The new position sticks as popups open and close, and moves back inside the map if the map area shrinks (a panel opens, the window narrows) |
resizable |
boolean |
false |
Let users resize the desktop popup from any edge or corner, like a window. It can't get smaller than the configured size (maxWidth by height; 100 px tall with "height": "auto") or bigger than 440 × 400 px (or the configured size, if that's larger), and it stays inside the map. The size sticks until the page reloads. Mobile keeps the bottom sheet |
closeOnMapClick |
boolean |
true |
Clicking empty map closes a click popup. Set false for popups that only close with X, Escape or another feature |
zoomToFeature |
boolean |
true |
A button that zooms to the feature (points fly to a fixed zoom) |
copyCoordinates |
boolean |
false |
A button that copies the feature's coordinates |
viewInGoogleMaps |
boolean |
false |
A button that opens the feature in Google Maps, with a pin at the exact point |
headerSize |
number |
14 |
Text size (px) of the header bar, next to the close button |
titleSize |
number |
16 |
Text size (px) of the title inside the popup. Templates without a title ignore it |
bodySize |
number |
14 |
Text size (px) of the fields table and body text |
Next to the feature¶
With "position": "feature", the popup opens above the feature (below it when there's no room), stays inside the map, and follows the map as it pans and zooms. A small arrow on its edge points at the feature: at a point itself, at the nearest spot on a line, and at the clicked spot inside a polygon. Paging through stacked features moves it to each one. Once a user drags or resizes the popup, it stays where they put it and the arrow goes away.
Text sizes¶
The three popup sizes reach the header bar through the shell and the inside of the popup through your templates: a template's title uses text-popupTitle and its body text-popupBody (see Templates). The built-in templates already do.
On mobile, a popup opens in the bottom sheet, over any widget that's open there. Closing the popup (X, or a tap on empty map) goes back to the widget as you left it, scroll position included. Tapping the open widget's button while a popup covers it does the same.
Hover popups show one feature at a time
With "trigger": "hover", a spot where features overlap shows only the top one, with no ◀ ▶ pager. Moving the mouse to the arrows would change what's under the pointer, so paging through stacked features only works with "click". Use "click" on layers whose features overlap.
Per-layer: layer.popup¶
Add a popup block to a layer to turn its popups on.
"popup": {
"displayType": "rich",
"title": "plant_name",
"header": "Power plant — {field:state}",
"fields": {
"primsource": "Energy Source",
"capacity_mw": "Capacity (MW)"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Set false to turn the popup off but keep its config |
trigger |
"click" \| "hover" |
from ui.popups |
Overrides the global trigger |
position |
as ui.popups.position |
from ui.popups |
Overrides the global position |
title |
string |
none | Property whose value becomes the popup title |
header |
string |
layer name | Header bar text, with per-feature placeholders (below) |
fields |
object of "column": "Label" |
all fields | Which columns to show, and their labels |
displayType |
string |
from ui.popups |
Template name from popups.tsx |
zoomToFeature |
boolean |
from ui.popups |
Overrides the global zoom-to-feature button |
copyCoordinates |
boolean |
from ui.popups |
Overrides the global copy-coordinates button |
viewInGoogleMaps |
boolean |
from ui.popups |
Overrides the global Google Maps button |
The last three appear as buttons at the popup's bottom right, opposite the pager for stacked features. The Google Maps button differs from the right-click menu's "Open in Google Maps": the popup button drops a pin on the feature, while the menu item matches the map's current zoom and drops no pin.
The fields dictionary¶
fields maps property names to labels, shown in the order you write them. To keep a name as is, repeat it as the label. Here passengers is renamed and STATE isn't:
Leave out fields to show every property the feature has, in its own order, labeled by column name.
Header placeholders¶
header replaces the default header text, the layer name. These placeholders are filled in for each feature:
| Placeholder | Resolves to |
|---|---|
{layerName} |
The layer's display name |
{group} |
The layer's group (empty if it has none) |
{field:NAME} |
The feature's NAME property (empty if missing) |
"header": "Airport — {field:city}" shows Airport — Denver for a feature whose city is Denver.
Three ways to turn a popup off¶
| Form | Meaning |
|---|---|
Leave out popup |
Off. The usual way |
"popup": false |
Off, explicitly |
"popup": { "enabled": false, … } |
Off, but the config stays, so you can turn it back on without rewriting the fields |
Templates: popups.tsx¶
A popup's look is a React component. Your project's popups.tsx exports a popupTemplates object that maps names to components, and a layer picks one with popup.displayType.
export const popupTemplates: PopupTemplates = {
default: DefaultPopup,
rich: RichPopup,
featureHighlights: FeatureHighlightPopup,
}
Each template receives:
| Prop | Type | Description |
|---|---|---|
feature |
GeoJSON.Feature |
The clicked feature |
fields |
Record<string, string> \| null |
The layer's fields, or null to show every property |
title |
string \| undefined |
The title value (from popup.title) |
layerId / layerName |
string |
The feature's layer |
The demo ships three templates:
default: a plain label and value list.rich: a card with a title in the brand color and a striped table.featureHighlights: the first field large at the top, the rest in a table below.
Writing your own¶
Write a component and register it. A minimal template:
const MyPopup: PopupTemplate = ({ title, fields, feature }) => {
const props = (feature.properties ?? {}) as Record<string, unknown>
return (
<div className="p-3 text-popupBody text-strongText">
{title && <h3 className="mb-2 text-popupTitle font-semibold">{title}</h3>}
{/* render fields… */}
</div>
)
}
export const popupTemplates: PopupTemplates = {
default: DefaultPopup,
myPopup: MyPopup, // used from layers.json as "popup": { "displayType": "myPopup" }
}
A typo turns popups off
If a layer names a displayType that isn't in popups.tsx, popups stay off for every layer, and the browser console names the missing template and lists the ones that exist.
In your template, use the kit's theme classes (text-text, text-strongText, text-primary, bg-surface, border-panelBorder and so on) instead of fixed colors like text-slate-900 or bg-white, and the popup sizes text-popupTitle / text-popupBody instead of Tailwind's text-sm or text-base. Then it follows the theme, dark presets included, and the size settings. The built-in rich and featureHighlights templates show how. See Theme.