A type-safe MapLibre style expression builder for TypeScript.
npm install git+https://github.com/canida-software/style-forge.git#distEnsure the peer dependency is installed:
npm install @maplibre/maplibre-gl-style-spec🎯 Fluent API First – The fluent, chainable API is the recommended way to use Style Forge.
// MapLibre JSON
const highlightColorJson = ['case', ['has', 'isSelected'], '#f97316', '#9ca3af'];
// Style Forge
const highlightColor = fx.when(fx.has('isSelected')).then('#f97316').else('#9ca3af');Direct imports like get, has, when are available as convenient shorthands.
import { fx } from 'style-forge';
// ---------------------------------------------------------------------------
// Example 1: Category-based color
// ---------------------------------------------------------------------------
// MapLibre JSON
const colorByCategoryJson = [
'match',
['get', 'category'],
'residential',
'#ffeb3b',
'commercial',
'#2196f3',
'industrial',
'#f44336',
'recreational',
'#4caf50',
'#9e9e9e',
];
// Style Forge
const colorByCategory = fx
.get('category')
.match({ residential: '#ffeb3b', commercial: '#2196f3', industrial: '#f44336', recreational: '#4caf50' })
.fallback('#9e9e9e');
// ---------------------------------------------------------------------------
// Example 2: Zoom-based opacity
// ---------------------------------------------------------------------------
// MapLibre JSON
const opacityByZoomJson = ['interpolate', ['linear'], ['zoom'], 0, 0.1, 10, 0.5, 20, 1.0];
// Style Forge
const opacityByZoom = fx.interpolate(['linear'], fx.zoom(), 0, 0.1, 10, 0.5, 20, 1.0);
// ---------------------------------------------------------------------------
// Example 3: Conditional size by magnitude
// ---------------------------------------------------------------------------
// MapLibre JSON
const sizeByMagnitudeJson = [
'case',
['>=', ['get', 'magnitude'], 5],
['interpolate', ['exponential', 2], ['get', 'magnitude'], 5, 10, 10, 50],
5,
];
// Style Forge
const sizeByMagnitude = fx
.when(fx.get('magnitude').gte(5))
.then(fx.interpolate(['exponential', 2], fx.get('magnitude'), 5, 10, 10, 50))
.else(5);
// ---------------------------------------------------------------------------
// Example 4: Layer with data-driven color and size
// ---------------------------------------------------------------------------
// MapLibre JSON
const earthquakeLayerJson = {
id: 'earthquakes',
type: 'circle',
source: 'earthquake-source',
paint: {
'circle-color': colorByCategoryJson,
'circle-radius': sizeByMagnitudeJson,
'circle-opacity': opacityByZoomJson,
},
};
// Style Forge
const earthquakeLayer = new Layer('circle', 'earthquakes', 'earthquake-source')
.circleColor(colorByCategory.forge())
.circleRadius(sizeByMagnitude.forge())
.circleOpacity(opacityByZoom.forge())
.visibility('visible');
// ---------------------------------------------------------------------------
// Example 5: Derived value with let / var
// ---------------------------------------------------------------------------
// MapLibre JSON
const scaledDensityJson = [
'let',
'pop',
['get', 'population'],
'area',
['get', 'area'],
['*', ['/', ['var', 'pop'], ['var', 'area']], 100],
];
// Style Forge - Functional syntax (recommended for complex expressions)
const scaledDensity = fx.$let({ pop: fx.get('population'), area: fx.get('area') }, ({ $var }) =>
$var.pop.divide($var.area).multiply(100),
);
// Style Forge - Builder syntax (simpler cases)
const scaledDensityBuilder = fx
.$let({ pop: fx.get('population'), area: fx.get('area') })
.in(fx.var('pop').divide(fx.var('area')).multiply(100));
// ---------------------------------------------------------------------------
// Example 6: Complex adaptive sizing with let
// ---------------------------------------------------------------------------
// MapLibre JSON
const adaptiveSizeJson = [
'let',
'magnitude',
['get', 'magnitude'],
'zoom',
['get', 'zoom'],
'baseSize',
8,
'maxSize',
24,
[
'case',
['<', ['var', 'zoom'], 10],
['var', 'baseSize'],
['<', ['var', 'zoom'], 15],
['interpolate', ['linear'], ['var', 'magnitude'], 0, ['var', 'baseSize'], 10, ['*', ['var', 'baseSize'], 1.5]],
['interpolate', ['exponential', 2], ['var', 'magnitude'], 0, ['*', ['var', 'baseSize'], 2], 10, ['var', 'maxSize']],
],
];
// Direct import shorthand - convenient alternative to fx
import { get, when, interpolate, $let } from 'style-forge';
// Style Forge's functional syntax shines for complex expressions
const adaptiveSize = $let(
{
magnitude: get('magnitude'),
zoom: get('zoom'),
baseSize: 8,
maxSize: 24,
},
({ $var }) => {
return when($var.zoom.lt(10))
.then($var.baseSize)
.when($var.zoom.lt(15))
.then(interpolate(['linear'], $var.magnitude, 0, $var.baseSize, 10, $var.baseSize.multiply(1.5)))
.else(interpolate(['exponential', 2], $var.magnitude, 0, $var.baseSize.multiply(2), 10, $var.maxSize));
},
);
// ---------------------------------------------------------------------------
// Example 7: Palette-based color
// ---------------------------------------------------------------------------
const palette = [
'#ff0000',
'#00ff00',
'#0000ff',
// ...
];
// MapLibre JSON (schematic)
const paletteColorJson = [
'case',
['has', 'id'],
[
'match',
['%', ['to-number', ['get', 'id']], palette.length],
0,
palette[0],
1,
palette[1],
2,
palette[2],
// ...
'#64748b',
],
'#64748b',
];
// Style Forge
import { get, has, when } from 'style-forge';
const paletteColor = when(has('id'))
.then(
get('id')
.toNumber()
.mod(palette.length)
.match(Object.fromEntries(palette.map((color, index) => [index, color] as const)))
.fallback('#64748b'),
)
.else('#64748b');
const advancedLayer = new Layer('fill', 'buildings-advanced', 'buildings-source', 'buildings')
.fillColor(paletteColor.forge())
.fillOpacity(0.8)
.visibility('visible');Basic Expressions
get(property),has(property)– feature access and existence.zoom()– current zoom level.literal(value)– literal values.globalState(key)– read from global state.elevation()– terrain elevation expression.
Conditionals & Match
when(condition).then(value).else(value)– fluent conditional builder.conditional(condition)– alias forwhen.match(input).branches({...}).fallback(value)– match expressions with branches.
Variable Bindings
$let(bindings)/Expression.$let(bindings)– scoped variable bindings using MapLibre$let(builder syntax).$let(bindings, callback)/Expression.$let(bindings, callback)– functional syntax for complex expressions.$var(name)/Expression.var(name)– reference a bound variable.
Mathematics & Constants
- Static helpers:
add(...),subtract(a,b),multiply(...),divide(a,b),mod(a,b),pow(a,b). - Fluent math:
expr.add(...),expr.subtract(...),expr.multiply(...),expr.divide(...),expr.mod(...),expr.pow(...). - Extra fluent math:
expr.sqrt(),expr.log10(). - Constants:
pi(),e(),ln2().
Comparison & Logic
- Comparisons:
eq(a,b),neq(a,b),gt(a,b),gte(a,b),lt(a,b),lte(a,b)or fluentexpr.eq(...), etc. - Logic:
and(...),or(...),not(expr)and fluentexpr.and(...),expr.or(...).
Type Conversion & Assertions
- Conversions:
toNumber(value),toString(value),toBoolean(value),toColor(value)or fluentexpr.toNumber(), etc. - Assertions:
string(value),number(value),boolean(value),array(value),object(value)or fluentexpr.string(),expr.array('string'), etc. - Type checks:
typeOf(value)/ fluentexpr.typeof(). - Null handling:
coalesce(...values)/ fluentexpr.coalesce(...).
Advanced Type & Formatting
collator(options?)– locale-aware string comparison configuration.format(...parts)– rich text formatting.- Fluent helpers:
expr.image()for sprite references,expr.numberFormat(options)for localized numbers,expr.resolvedLocale(),expr.isSupportedScript().
Strings & Lookups
- String helpers:
concat(...values),upcase(value),downcase(value)or fluentexpr.concat(...),expr.upcase(),expr.downcase(). - Lookups (fluent only):
expr.length(),expr.at(index),expr.slice(start, end?),expr.in(collection),expr.indexOf(item, fromIndex?).
Interpolation & Stepping
- Static interpolation:
interpolate(interpolation, input, ...stops)– e.g.interpolate(['linear'], zoom(), 0, 0.1, 20, 1.0). - Fluent interpolation:
expr.interpolate(interpolation, ...stops), plus color-space variantsexpr.interpolateHcl(...),expr.interpolateLab(...). - Static steps:
step(input, min, ...stops). - Fluent steps:
expr.step(min, ...stops).
new Layer(type, id?, source?, sourceLayer?)– create a layer..fillColor(value),.circleRadius(value), etc. – set paint properties..filter(expression)– add layer filter..visibility('visible' | 'none')– control layer visibility..minZoom(value),.maxZoom(value)– set zoom constraints.
npm run buildnpm install
npm run lint
npm run format
npm run dev