Framework-independent TypeScript utilities for generating MapLibre / Mapbox sprite assets from SVG icons.
The package includes a small React example, but the core library does not depend on React.
- Parse SVG text into icon inputs.
- Reject active or externally referenced SVG content at the import boundary.
- Normalize SVG file names into map icon IDs.
- Pack icons with
maxrects-packer. - Generate MapLibre / Mapbox-compatible
sprite.json. - Render transparent
sprite.pngin the browser. - Export
map-sprite.zipcontents with both normal and retina assets:sprite.pngsprite.jsonsprite@2x.pngsprite@2x.json
npm install map-spriteimport { createSprite, exportSpriteZip, parseSvgText } from "map-sprite";
const svgText = '<svg width="32" height="32" viewBox="0 0 32 32"></svg>';
const icon = parseSvgText(svgText, "Gas Valve.svg");
const sprite = createSprite([icon]);
console.log(sprite.json);
const zipBlob = await exportSpriteZip(sprite);Serve the generated sprite files over HTTP and set the style sprite field to the base URL without a file extension:
const style = {
version: 8,
sprite: "https://example.com/sprites/demo/sprite",
sources: {
test: {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [116.397, 39.908]
},
properties: {
icon: "gas-valve"
}
}
]
}
}
},
layers: [
{
id: "test-icon",
type: "symbol",
source: "test",
layout: {
"icon-image": ["get", "icon"],
"icon-size": 1,
"icon-allow-overlap": true
}
}
]
};MapLibre / Mapbox automatically request the matching files:
https://example.com/sprites/demo/sprite.json
https://example.com/sprites/demo/sprite.png
https://example.com/sprites/demo/sprite@2x.json
https://example.com/sprites/demo/sprite@2x.png
The icon-image value must match a key in sprite.json, not the original SVG file name.
parseSvgText is the intended import boundary for user-provided SVG files. It stores the original SVG text for rendering, but rejects SVG content that can execute code or fetch external resources:
<script>elements- event handler attributes such as
onload <foreignObject>HTML embeddinghref/xlink:hrefvalues that start withhttp:,https:,data:, orjavascript:
parseSvgText('<svg width="16" height="16"><script>alert(1)</script></svg>', "bad.svg");
// throws: Unsafe SVG content in "bad.svg".If you bypass parseSvgText and construct SvgIconInput objects manually, validate or trust the SVG text before passing it to createSprite, renderSpritePng, exportSpriteZip, or MapSpriteEditor.
The package also exposes a DOM-mounted editor, similar to map SDK initialization:
import { MapSpriteEditor } from "map-sprite";
const editor = new MapSpriteEditor({
container: document.getElementById("sprite-editor")!,
logic: "max-edge",
padding: 2,
preserveOrder: true,
themeColor: "#3fb572",
onChange({ sprite }) {
console.log(sprite.json);
}
});
editor.setThemeColor("#2f9e6a");
// Later, when unmounting:
editor.destroy();The editor owns its UI and supports SVG upload, drag-and-drop import, icon order reordering, icon rotation, layout strategy switching, gap configuration, theme color switching, transparent checkerboard preview, JSON preview, and ZIP export.
The editor also owns its icon array internally. Constructor icons, setIcons(icons), getState().icons, and onChange({ icons }) use array copies so callers cannot mutate editor state by keeping a reference to a previously passed or returned array.
Editor layout modes:
max-edgeandmax-areausemaxrects-packer; icon dragging is disabled because positions are generated by the packer. ToggleKeep orderto preserve import order or let the packer search compact layout candidates.customfreezes the current positions and enables dragging. Dropping one icon on another swaps those two icons in the order, then recalculates custom positions with the configured gap without usingmaxrects-packer.
Switch packing logic when creating a sprite:
createSprite(icons, { logic: "max-edge" }); // default
createSprite(icons, { logic: "max-area" });Set spacing between SVG icons:
createSprite(icons, { padding: 4 });Rotate an icon in 90-degree steps before packing:
const rotatedIcon = {
...icon,
rotation: 90
} as const;
const sprite = createSprite([rotatedIcon]);
console.log(sprite.json[rotatedIcon.name].width); // uses the rotated bounding boxPreserve the caller-provided icon order instead of letting addArray sort inputs first:
createSprite(icons, { preserveOrder: true });exportSpriteZip includes normal and @2x assets by default. To export only one form:
await exportSpriteZip(sprite, { includeNormal: true, includeRetina: false });
await exportSpriteZip(sprite, { includeNormal: false, includeRetina: true });normalizeIconName(fileName: string): string
parseSvgText(svgText: string, fileName: string): SvgIconInput
createSprite(icons: SvgIconInput[], options?: SpriteOptions): SpriteResult
renderSpritePng(sprite: SpriteResult, options?: RenderSpriteOptions): Promise<Blob>
exportSpriteZip(sprite: SpriteResult, options?: ExportSpriteZipOptions): Promise<Blob>
createSpriteJson(icons: PackedIcon[], pixelRatio?: 1 | 2): SpriteJson
createRetinaSpriteJson(icons: PackedIcon[]): SpriteJson
resolveSpriteOptions(options?: SpriteOptions): Required<SpriteOptions>Default packing options:
{
maxWidth: 1024,
maxHeight: 1024,
padding: 2,
border: 1,
smart: true,
pot: false,
square: false,
allowRotation: false,
logic: "max-edge",
preserveOrder: false
}Packer-level rotation is always disabled because automatic rectangle rotation would desynchronize the rendered icon from downstream map usage. Per-icon rotation is supported in 90-degree steps and is reflected in the rendered PNG and generated JSON bounds.
Packing logic options:
"max-edge": sorts by longest edge and picks tighter edge fits. This is the default."max-area": sorts by area and picks lower wasted-area fits.
When preserveOrder is false, packing tries multiple deterministic order and width candidates, then keeps the single-sprite result with the smallest area.
Run the included example:
npm install
npm run devThe example supports SVG upload, drag-and-drop import, layout mode switching, gap changes, theme color changes, transparent checkerboard canvas preview, click selection, custom-mode reorder dragging, rotation, deletion, JSON preview, and ZIP export.
It also includes a MapLibre Test view. Upload SVG icons in Sprite Editor first, then switch to MapLibre Test. The test view uses the current editor onChange output, renders sprite.png, sprite.json, sprite@2x.png, and sprite@2x.json from that same SpriteResult, serves them through the local Vite dev server, sets the MapLibre style sprite URL to that HTTP endpoint, and renders the icons through a real symbol layer.
npm run format:check
npm run format
npm test
npm run typecheck
npm run buildnpm run build uses vite.lib.config.ts for the distributable library bundle and tsconfig.build.json for declaration output. The default vite.config.ts is kept for the React example and Vitest environment.
The ZIP export test uses an injected PNG renderer so core ZIP behavior can be verified in Node without a real browser canvas.
See CHANGELOG.md for release notes.