Generating Sprite Sheets for MapLibre Styles
spreet --ratio 1 icons/ dist/sprite packs every SVG in a directory into one PNG and writes the JSON index that maps icon names to positions. Doing it well means two things beyond running the command: deriving the icon list from the style rather than from a folder, and producing a byte-stable atlas so an unchanged icon set does not republish on every build.
When to Use This
Whenever a style uses icon-image. That includes almost every basemap — POI markers, transport symbols, one-way arrows, shields — and any overlay that places symbols. The sprite is required even for a single icon; there is no way to reference an image from a style without one.
Specification Detail
| Output | Purpose | Requested when |
|---|---|---|
sprite.json |
Name → position, size and pixel ratio | Always |
sprite.png |
The 1× atlas | Always |
[email protected] |
Index for the 2× atlas | Device pixel ratio above 1 |
[email protected] |
The 2× atlas | Device pixel ratio above 1 |
The style names the base URL only:
{ "sprite": "https://tiles.example.com/v43/sprite" }
Each index entry describes one icon:
| Key | Meaning |
|---|---|
x, y |
Top-left position in the atlas |
width, height |
Size in atlas pixels |
pixelRatio |
1 or 2 — how many atlas pixels per CSS pixel |
sdf |
true for a signed-distance-field icon that icon-color can tint |
Production Command
Derive the list from the style, generate both ratios, and verify:
#!/usr/bin/env bash
set -euo pipefail
STYLE=style/base.json
SRC=icons
OUT=dist/v43
# 1. Which icons does the style actually name?
jq -r '
[ .layers[] | (.layout // {})["icon-image"]
| if type == "string" then .
elif type == "array" then (.. | strings)
else empty end ]
| unique | .[]
' "$STYLE" | grep -v '^$' | sort -u > icons.manifest
# 2. Stage only those icons, so the atlas holds nothing unused
rm -rf build/icons && mkdir -p build/icons
while read -r NAME; do
[ -f "$SRC/$NAME.svg" ] || { echo "missing source icon: $NAME"; exit 1; }
cp "$SRC/$NAME.svg" "build/icons/$NAME.svg"
done < icons.manifest
# 3. Pack both pixel ratios
mkdir -p "$OUT"
spreet --unique --ratio 1 build/icons "$OUT/sprite"
spreet --unique --ratio 2 build/icons "$OUT/sprite@2x"
# 4. Every manifest icon must be in the index
comm -23 <(sort icons.manifest) <(jq -r 'keys[]' "$OUT/sprite.json" | sort) \
| sed 's/^/not packed: /' | grep . && exit 1
echo "packed $(wc -l < icons.manifest) icons"
Step two is what keeps the atlas honest. Packing a whole design-system directory produces a sheet several times larger than needed, and every byte of it is fetched by every reader before an icon can be drawn.
Reproducibility
An atlas is packed by a bin-packing algorithm, and a different input order can produce a different layout with identical content. That matters because a reordered atlas is new bytes, which means a new content hash, which republishes an asset nothing about which has actually changed — and invalidates every reader’s cached copy.
Two measures make the output stable. Feed the packer a deterministic file order, which the while read loop above does by consuming a sorted manifest. And pin the packer version, since a bin-packing improvement between releases legitimately changes the layout.
With both in place, an unchanged icon set produces a byte-identical atlas, and the publish step can skip it entirely.
Interaction Effects
With SDF icons and theming. An SDF icon is a single-channel distance field that icon-color can tint at runtime, which lets one sheet serve light and dark themes. A full-colour icon cannot be tinted, so a themed map needs either SDF icons or a second sheet — and the second sheet is where theme drift creeps in.
With expressions. An icon-image built with concat or match cannot be enumerated statically, so the manifest is a lower bound. For styles that build icon names dynamically, add the possible values explicitly to the manifest rather than hoping the extraction found them.
With versioning. The index and the PNG must be deployed together and cached together. Publishing them under a version prefix with immutable headers is what guarantees a reader never mixes an old index with a new atlas.
Performance Impact
| Icons packed | 1× PNG | 2× PNG | Pack time |
|---|---|---|---|
| 40 | 34 KB | 118 KB | 0.4 s |
| 180 | 96 KB | 340 KB | 1.1 s |
| 600 (whole design system) | 290 KB | 1.1 MB | 3.6 s |
The third row is the cost of packing a directory instead of a manifest: roughly a megabyte of atlas, fetched before any icon renders, of which a basemap typically uses a third.
Common Mistakes
Packing the source directory. Ships icons nobody references and inflates the fetch on the critical path.
Shipping only one pixel ratio. 1×-only is soft on most devices; 2×-only means no icons at all on a 1× display, since MapLibre requests the plain names first.
Serving the index and atlas with different cache lifetimes. A fresh index against a stale PNG draws the wrong icons, which is far more confusing than a missing one.
Assuming an unfound icon errors. MapLibre reserves the symbol’s space and draws nothing, so the layout looks right and the icons are simply absent.
FAQ
Can one style use several sprites?
MapLibre supports a sprite array with named sheets, referenced as sheet:icon. It is useful when one team owns the basemap icons and another owns an overlay’s, and it costs one extra fetch per sheet.
Do I need a 3× sprite?
Almost never. The visual difference above 2× is imperceptible for icons at typical map sizes, and it doubles the atlas again.
How do I add an icon without republishing everything?
You cannot — the atlas is one image and adding to it reflows the layout. That is precisely why the sheet is versioned and immutable rather than patched.
Does --unique change the index?
It deduplicates identical images, so two names can map to the same atlas position. The index still carries both names, so nothing in the style changes.
Related
- Sprite and Glyph Pipelines — the parent topic and the glyph half of the same deployment.
- Debugging Missing Icons and Fonts in MapLibre — telling a 404 from a name mismatch.
- MapLibre GL JSON Structure — where
spritesits in the style document. - Theme Inheritance Patterns — why SDF icons matter for a two-theme map.