Debugging Missing Icons and Fonts in MapLibre
Labels or icons disappearing is one symptom with four causes, and MapLibre reports none of them. The network panel distinguishes three of them immediately; the fourth — a name that resolves but does not match — needs one comparison. Work through them in that order and the cause is usually found before the second cup of coffee.
When to Use This
Text renders nowhere on the map. Symbol layers place empty gaps where icons should be. Or either of those happens only in staging, only on a phone, or only for one layer.
The Four Causes
The last branch is worth checking first despite being listed last, because it is the cheapest: if MapLibre never requested the asset, no amount of debugging the asset will help.
Production Command
Four commands, in order of how much they rule out:
BASE=https://tiles.example.com/v43
ORIGIN=https://maps.example.com
# 1. Does the sprite index exist at all?
curl -sfI "$BASE/sprite.json" | head -1
curl -sfI "$BASE/sprite.png" | head -1
curl -sfI "$BASE/[email protected]" | head -1 # requested on high-DPI screens
# 2. Does the exact glyph URL exist? Encode the fontstack exactly as MapLibre does.
STACK=$(jq -r '.layers[] | select(.layout["text-font"]) | .layout["text-font"] | join(",")' \
style.json | head -1)
curl -sfI "$BASE/../fonts/$(printf %s "$STACK" | jq -sRr @uri)/0-255.pbf" | head -1
# 3. Does CORS allow the map's origin?
curl -sI -H "Origin: $ORIGIN" "$BASE/sprite.json" \
| grep -i "access-control-allow-origin" || echo "NO CORS HEADER"
# 4. Do the names in the style exist in the sprite index?
curl -s "$BASE/sprite.json" | jq -r 'keys[]' | sort > /tmp/have
jq -r '[.layers[] | (.layout // {})["icon-image"] | strings] | unique | .[]' style.json \
| sort > /tmp/want
comm -23 /tmp/want /tmp/have | sed 's/^/style names an icon that is not packed: /'
Command three is the one people skip, and it is the difference between “works locally, fails deployed” and a five-minute fix. A missing Access-Control-Allow-Origin produces a console error naming CORS, but the symptom on the map is identical to a 404.
Reading the Symptoms
Text missing everywhere, icons fine. A glyph problem. Almost always the fontstack directory name — check the URL-encoded path against what is on disk, character for character.
Icons missing, text fine. A sprite problem. If sprite.json is a 200, the index loaded and the icon name is not in it; compare with command four.
Icons blank only on a phone. The @2x pair is missing. The 1× files resolve, the high-DPI request 404s, and MapLibre draws nothing rather than falling back.
Everything missing on one layer only. Not an asset problem. The layer’s filter matches nothing, its zoom range excludes the current view, or its text-field evaluates to null — which is a tile attribute question rather than an asset one.
Works locally, fails deployed. CORS, nearly every time. Local development serves the style and its assets from one origin, so no policy is exercised.
Interaction Effects
With the style validation suite. Every check on this page belongs in the dependency phase of style validation, where it runs before deploy rather than after a report. The commands are the same; only the timing differs.
With versioned deploys. A sprite index cached from one version against an atlas from another draws the wrong icons — a symptom that looks like a packing bug and is a caching one. Both files must share a version prefix and an immutable policy.
With themes. A dark-theme style pointing at a light-theme sprite resolves every name successfully and draws icons that are invisible against the background. Nothing in this diagnostic tree catches that, because nothing is failing.
Performance Impact
Debugging aside, these assets sit on the path to the first labelled frame. A sprite index and image plus one glyph range is typically 150–300 KB and three requests, all of which must complete before symbols appear.
That is why a map often renders geometry promptly and then populates with labels and icons a moment later, and why serving both bundles from the same origin as the tiles — no extra DNS lookup, no extra TLS handshake — is worth doing even though the bytes are modest.
Common Mistakes
Reconstructing the URL by hand. The fontstack encoding is easy to get subtly wrong. Copy the failing URL out of the network panel.
Testing CORS without an Origin header. curl without one gets a response that tells you nothing about the browser’s outcome.
Assuming a 200 means success. A sprite index that loads and lacks the icon is a 200 and an empty map symbol.
Debugging the style first. It is the least likely cause and the most time-consuming to read.
FAQ
Why does MapLibre not log a warning for a missing icon?
Because an absent icon is indistinguishable from an intentionally conditional one — an icon-image expression that legitimately evaluates to nothing for some features. Warning on it would produce false positives on most real styles.
Can I use a fallback icon?
Yes: an icon-image expression wrapped in coalesce with a known-present name renders a placeholder rather than nothing, which turns a silent failure into a visible one.
Why do labels sometimes appear after a delay?
Because glyph ranges are fetched lazily, only when a label that needs them is laid out. Panning into an area with different characters can trigger a new range fetch mid-session.
Does an @2x sprite need its own index?
Yes — [email protected] as well as [email protected]. Publishing only the image leaves the index 404ing and no icons at all on high-DPI screens.
Related
- Sprite and Glyph Pipelines — the parent topic: building and publishing both bundles.
- Generating Sprite Sheets for MapLibre Styles — where the names in the index come from.
- Hosting Glyph Ranges for Custom Fonts — the fontstack naming rule behind most label failures.
- Layer Filter Synchronization — when the layer, not the asset, is the reason nothing draws.