The problem
MapLibre renders vector tile polygons with solid colour fills. Solid colour works for most use cases, but patterns unlock three things colour alone cannot do.
First, accessibility. Around 8% of men have colour vision deficiency. Distinctions that are obvious to most people — red vs green, blue vs purple — blur together. Patterns work for everyone, regardless of how they perceive colour.
Second, print. Maps printed in grayscale lose every colour-based distinction. Patterns survive the transition to black and white.
Third, two-dimensional encoding. Solid fill encodes one variable: colour. Patterns add a second visual channel. You can use colour for one metric and pattern for another, which opens up readability in ways colour alone cannot.
The problem is that MapLibre’s WebGL renderer does not accept SVG directly. Every pattern must go through a conversion pipeline — SVG to Canvas to ImageData to WebGL texture — and be registered with map.addImage(). This plugin wraps that pipeline, adds caching, and provides expression builders so patterns work with any GeoJSON source.
How it works
The plugin uses textures.js — a tiny SVG pattern library — to generate 28 built-in patterns: lines, circles, hexagons, waves, paths, and more. Each pattern is rendered to a hidden canvas at 4x resolution (to stay sharp on Retina displays), converted to ImageData, and registered with the map.
const patterns = new PolygonPatterns(map, { prewarmOnLoad: true });
await patterns.ensure("lines-crossed", "#c65b2e");
The ensure(patternId, colour) method is the core. It checks a cache first — if that pattern+colour combination was already generated, it returns instantly. Otherwise it runs the full SVG→Canvas→WebGL pipeline and registers the image.
Patterns are referenced in a layer through fill-pattern. The plugin provides two expression builders:
buildFillPatternExpr()— readsgeoglify:patternEnabled,geoglify:patternId, andgeoglify:patternColorfrom each feature’s properties and builds amatchexpression that resolves the correct image name.buildFillOpacityExpr(fallback)— readsgeoglify:patternOpacityand returns a paint expression, with a fallback for features that have no pattern.
This means every polygon in a single layer can have a different pattern and colour. The expression routes each feature to its registered image based on its properties.
Colorize
Adjacent polygons should not share the same pattern. The colorize() static method assigns patterns to polygons so that neighbours always get different ones.
const assignment = PolygonPatterns.colorize(
features.map((f) => ({ id: f.id, coordinates: f.geometry.coordinates })),
PATTERN_IDS,
);
// Returns: [{ id: "PT", patternId: "lines-crossed" }, ...]
The method was ported from geoglify.com. I wrote about the algorithm — R-tree spatial filtering, edge hashing for exact adjacency, and randomised greedy graph coloring — in detail in the previous article Map Patterning: Assigning SVG Textures to Hundreds of Adjacent Polygons. The short version: it finds neighbours in ~5ms for 242 countries and assigns 28 patterns with zero conflicts.
How to use
Install with npm:
npm install @leoneljdias/maplibre-gl-polygon-patterns
Or from a script tag (UMD):
<script src="https://unpkg.com/@leoneljdias/maplibre-gl-polygon-patterns/dist/index.umd.js"></script>
<script>
const patterns = new maplibregl.PolygonPatterns(map, options);
</script>
The “random mode” flow — assign a random pattern+colour to every polygon in a dataset — follows five steps:
- Pick colours and pattern IDs
- (Optional) Call
PolygonPatterns.colorize()so adjacent polygons get different patterns - Set
geoglify:patternEnabled,geoglify:patternId,geoglify:patternColor, andgeoglify:patternOpacityon each feature - Call
patterns.ensure()for every unique pattern+colour combination - Use
buildFillPatternExpr()andbuildFillOpacityExpr()in your layer paint properties
The demo shows this end to end with world countries and a Regenerate button.
Bundle size
The plugin is about 51KB minified (textures.js and d3-selection are bundled in the dist). The colorize() algorithm adds no extra dependencies — it uses plain JavaScript with a built-in R-tree implementation.