Guides
Migrating from Mapbox.
X-GIS reads the Mapbox Style Spec — paste a style.json
into /convert
and get an equivalent .xgis source. This page
explains what carries across, what's heuristically lossy, and the
conceptual differences between the two engines.
Mental model differences
Mapbox GL JS is an imperative library — you instantiate
a Map, call addLayer(),
wire styling through paint property setters, ship glue code
in JS. xgis is a language: the source is the program,
and the compiler emits the WGSL.
Mapbox
- • Imperative API —
map.addLayer() - • JSON style spec parsed at runtime
- • Expressions evaluated by a tree walker per frame
- • GLSL shaders hand-written for each paint property
- • Symbol layers (text, icons) first-class
xgis
- • Declarative —
layer x { … } - • Compile-time IR + WGSL codegen
- • Zoom-driven values lower to GPU uniforms
- • Per-feature expressions baked into vertex attributes
- • Symbol layers not yet implemented (text rendering pending)
What this means in practice
text-field, text-color,
text-size (zoom-interpolated),
text-halo-*,
text-anchor,
text-transform,
text-offset,
text-rotate,
text-letter-spacing,
text-max-width + text-line-height + text-justify (multiline wrap),
text-font (font-stack fallback),
text-allow-overlap,
text-ignore-placement and
text-padding (greedy bbox collision)
all map to label-* utilities.
symbol-placement: line renders
road / waterway / highway names along their geometry, rotated
to the local tangent (Batch 1d v1 — single-segment placement
per feature; per-glyph along-curve and repeat-along-line
ship in v2). Icons depend on the upcoming sprite atlas.
Using the converter
Open /convert,
click a preset chip (Liberty / Bright / Positron — three OpenFreeMap
styles) or paste a URL pointing at any style.json.
The page emits an .xgis source and
a count badge showing how many features got dropped or rewritten.
Or skip the build step entirely — the splice-form
import "url" directive
fetches and runs the converter at runtime, so a one-liner inside
your xgis source drops a full base map onto the canvas. Auto-detect
only fires when the URL returns JSON shaped like a Mapbox v8 style
(version + layers);
plain xgis modules go through unchanged.
import "https://tiles.openfreemap.org/styles/bright"
source places { type: geojson, url: "places.geojson" }layer my_pois { source: places | fill-rose-500 fill-opacity-80}The converter is also exported from the compiler package — call it from your build pipeline if you want to bake the converted xgis into your bundle (skips the runtime fetch).
import { convertMapboxStyle } from '@xgis/compiler'
const style = await fetch('https://tiles.openfreemap.org/styles/bright') .then(r => r.json())const xgis = convertMapboxStyle(style)// xgis is a string — feed to <XGISMap>.run() directly.Compatibility matrix
- • Sources: vector (PMTiles + TileJSON), raster, GeoJSON
- • Layers: background, fill, line, fill-extrusion, symbol (text)
- • Paint: fill-color/-opacity, line-color/-width/-dasharray/-opacity, fill-extrusion-color/-opacity/-height/-base
- • Symbol text: text-field, text-color, text-size (zoom-interpolated), text-halo-color/-width, text-anchor (5-way), text-transform, text-offset, text-rotate, text-letter-spacing, text-max-width + text-line-height + text-justify (multiline), text-font stack, text-allow-overlap, text-ignore-placement, text-padding (collision),
symbol-placement: line+line-center(along-path) - • Filters: legacy + expression form (==/!=/</>/all/any/in/!in/has/!has)
- • Expressions: get / coalesce / case / match / arithmetic (+ - * / % ^) / min / max / abs / ceil / floor / round / sqrt / sin / cos / tan / asin / acos / atan / ln / log10 / log2 / pi / e / ln2 / concat / length / downcase / upcase / step (N-stop) / let+var / to-string / to-number / to-boolean / to-color / rgb / rgba
- • Zoom:
interpolate(zoom, …)with linear curve
- •
interpolatecurve type (exponential / cubic-bezier) folded to linear - •
fill-translate,line-translatedropped - •
fill-outline-coloremitted as a separate stroke layer when paired - •
$type/$idfilters dropped (geometry type implied by utility) - •
text-anchordiagonals (top-left / etc.) collapse to dominant axis - •
text-fontresolved by browser glyph-fallback chain (no opentype-sdf bake step) - •
fill-pattern/line-patterndropped (bitmap atlases unimplemented)
- • Icon symbols + sprite atlas (icon-image / icon-size / icon-color)
- •
text-keep-uprightper-glyph flip on curves - •
symbol-spacingrepeat-along-line (single-label per feature in v1) - •
text-writing-mode: vertical(CJK vertical text) - • Circle, heatmap, hillshade, sky layers
- •
number-format,format(rich text) - •
distance,within(geometry predicates) - • Top-level
light,fog,terrain
Expression mapping
Common Mapbox expressions and their xgis equivalents. The converter applies these automatically — this is for when you're reading converter output and want to know what changed.
Zoom-driven width
"line-width": [ "interpolate", ["linear"], ["zoom"], 11, 1, 19, 2.5]| stroke-[interpolate(zoom, 11, 1, 19, 2.5)]Property-driven color
"fill-color": [ "match", ["get", "class"], "park", "#cfe7c1", "water", "#a4c8d5", "#dadada"]| fill match(.class) { "park" -> #cfe7c1 "water" -> #a4c8d5 _ -> #dadada }Coalesce fallback
"fill-extrusion-height": [ "coalesce", ["get", "render_height"], ["get", "height"], 5]| fill-extrusion-height-[.render_height ?? .height ?? 5]Filter — legacy form
"filter": [ "all", ["==", "$type", "LineString"], ["==", "class", "highway"]]filter: .class == "highway"// $type dropped — geometry type// is implied by the utility usedCaveats & gotchas
Symbol layers are silently skipped
TileJSON URLs need the right type
.pmtiles extension
are emitted as type: tilejson — the runtime
fetches the manifest, then the per-tile .pbf.
If the host doesn't send Access-Control-Allow-Origin,
all tile fetches fail silently (the catalog stays empty).
Curve type loses subtlety
["exponential", base] curves get
flattened to linear. For a smooth-feel close to the original add
more stops in between — `interpolate(zoom, 11, 1, 14, 1.5, 18, 4)`
approximates an exponential rise better than two endpoints alone.
Round-trip is not exact
Specifications
See also