Style file format
For developers: the JSON file that Style (JSON) in the Download dialog gives you, and the JSON Schema that checks it. Your app does not need this file; it loads the .style file (see Download and use in your app). Use the JSON to keep a copy of a style, upload it to the portal, or write or check a style with your own tools.
To have an AI assistant write a style, give it the Studio llms.txt, which lists the four files it needs: this page, Map data sources, the layer types reference and the JSON Schema. Check what it writes with the schema (see The JSON Schema), then upload it to the portal and open it in Studio (see Start from a style file). An assistant cannot write a font: leave fonts_section out, and add the font in Studio afterwards (see Fonts).
Coming from other map style formats
The format is not the JSON style format that other web map libraries use. If you, or an AI assistant, have written styles in that format, these are the differences that matter:
| In other map style formats | In a Magic Lane style file |
|---|---|
layers at the top of the file | Layers sit in map_schemes_section.map_schemes[0].definition.layers. |
"source-layer": "water" names the data | The layer's type and sourcedata_id together name the data. See Map data sources. |
Expressions such as ["interpolate", ...] | Every paint property is a stop list, even a constant. See Paint values. |
Colours as "#ff8800" or rgba() | Colours are integers. See Colours. |
| The first layer is drawn at the bottom | The first layer is drawn on top; background is the last layer. |
"id": "water" names the layer | id is a number that goes down from layer to layer and ends at 1; the name goes in COMMENT. |
| Zoom 0 to 22 | Zoom 0 to 17. |
"text-field": "{name}" | Layers on map data draw their names by themselves; label_text is for your own data. See Text. |
"filter": ["==", ...] | A filter object. See Filters. |
| A sprite sheet | Images sit inside the file, in icons_section. See Images. |
text-font on each layer, with a glyphs URL | One font for the whole style, as font files inside the file, in fonts_section. See Fonts. |
The file
A style file is one JSON object with three sections, and a fourth when the style has its own font:
| Key | What it holds |
|---|---|
sources_list | { "sources": [...] }. Usually empty: layers use built-in sources. The external satellite source, satelliteExternal, is declared here. |
map_schemes_section | { "COMMENT": "...", "map_schemes": [ scheme ] }, with exactly one scheme. |
icons_section | { "COMMENT": "...", "icons": [ ... ] }, the style's images. See Images. |
fonts_section | Optional. { "regular": "data:font/ttf;base64,...", ... }, the style's font. See Fonts. |
The scheme:
| Key | What it holds |
|---|---|
id | Always 1. |
name | The style's name. |
groups_section | { "groups": [ { "id", "name", "collapsed"?, "parent_id"? } ] }, the groups of the layer list. parent_id names the parent of a nested group. |
definition | { "layers": [ layer, ... ] }. The first layer is drawn last, on top. |
Drawing order
The order of definition.layers is the drawing order: the first layer is drawn on top, the last at the bottom. The official templates use this order, from top to bottom:
atmosphere, if the style has one, then POIs, road names, one-way arrows and road shields- City, area, country and water labels
- The layers an app draws at run time: routes, traffic, drawings
- Borders, then roads
- Buildings
- Lines that are not roads (rail, runways, narrow water lines), then areas: lakes and rivers (Water) above parks and other land use
- Island, Water Sea and Land
background, always last. The templates give it the water colour.
A layer
| Key | Required | What it holds |
|---|---|---|
COMMENT | yes | The layer's name. |
id | yes | A number in drawing order: the first layer has the highest id, the last has 1, and each id is lower than the one before. Gaps are allowed; the official templates have them. |
type | yes | The layer type, such as polygon, line, symbol, street or areaname. See the layer types reference. |
source | no | A built-in source: map, overlay, satellite, satelliteExternal, elevation, weather (wind) or contourpolyline (contour lines). |
sourcedata_id | no | The data the layer draws: a public data source's number, or the tileset number of one of your data sources. The same number means different data in different layer types; see Map data sources. One number per layer. |
minzoom, maxzoom | no | The zoom range the layer is drawn in, 0 to 17. |
group_id | no | The id of the layer's group. |
filter | no | Which features are drawn. See Filters. |
label_text | no | The text a symbol layer draws. See Text. |
metaCategoryID | no | The point-of-interest category a POI layer draws; the engine draws that category's icon. |
contour_interval | no | The interval between lines, on contour layers. |
isTemplate | no | 1 on layers an app draws at run time (routes, traffic). Leave it out on other layers. |
paint | yes | The paint properties. See Paint values. |
Paint values
Every paint property is a stop list, even when it does not change with zoom:
"line-width": {
"line_width_stops": {
"line_width_stop": [
{ "level_id": 5, "width": 0.5 },
{ "level_id": 10, "width": 1 },
{ "level_id": 13, "width": 1.75 }
],
"easing_type": 1
}
}
These are the first three stops of the Residential street line in Basic 1.
Text sizes and icon sizes are millimetres on screen. Line widths are not screen units: the map engine scales them with the camera height, so start from the values in the official templates. A border width (line-outline-width) is added on each side of the line.
level_id is the zoom of the stop, 0 to 17; a constant is a single stop. easing_type sets how the value moves between stops (see Easings). The one exception is 3dMesh (0 for 2D, 1 for 3D), written as a bare number under paint.
Colours are integers; see Colours.
Each property has its own wrapper, stop array and value key:
| Property | Wrapper | Stop array | Value key |
|---|---|---|---|
fill-color, fill-outline-color, line-inner-color, line-outer-color, text-color, text-outline-color, background-symbol-color | color_stops | color_stop | color |
fill-opacity, fill-outline-opacity | opacity_stops | opacity_stop | opacity |
line-width, line-outline-width | line_width_stops | line_width_stop | width |
line-blur | line_blur_stops | line_blur_stop | blur |
text-size, text-outline-size | text_size_stops | text_size_stop | size |
text-padding | text_padding_stops | text_padding_stop | size |
text-max-width | text_max_width_stops | text_max_width_stop | size |
text-row-height | text_row_height_stops | text_row_height_stop | size |
icon-size | icon_size_stops | icon_size_stop | size |
icon-padding | icon_padding_stops | icon_padding_stop | size |
icon | icon_stops | icon_stop | custom_icon |
text-rotation-angle | angle-stops (with a hyphen) | angle_stop | angle |
icon-rotation-angle | angle_stops | angle_stop | angle |
translate-on-X, translate-on-Y | translate_stops | translate_stop | value |
offset-on-X, offset-on-Y | offset_stops | offset_stop | value |
text-anchor | text_anchor_stops | text_anchor_stop | intvalue |
text-transform | text_transform_stops | text_transform_stop | intvalue |
text-style | text_style_stops | text_style_stop | intvalue |
text-justify | text_justify_stops | text_justify_stop | intvalue |
text-overlap, text-optional, text-placement | text_overlap_stops, text_optional_stops, text_placement_stops | text_overlap_stop, text_optional_stop, text_placement_stop | boolvalue |
icon-overlap, icon-optional, icon-placement | icon_overlap_stops, icon_optional_stops, icon_placement_stops | icon_overlap_stop, icon_optional_stop, icon_placement_stop | boolvalue |
Options are stored as numbers:
| Property | Values |
|---|---|
text-anchor | left-top 6, left-center 4, left-bottom 7, center-top 2, center-center 9, center-bottom 1, right-top 8, right-center 3, right-bottom 5 |
text-transform | none 0, lowercase 1, uppercase 2 |
text-style | normal 0, bold 1, italic 2 |
text-justify | left 1, center 4, right 7 |
Which properties each layer type takes is in the layer types reference.
Colours
A colour is one 32-bit integer in ABGR order: (a << 24) | (b << 16) | (g << 8) | r, where a is the opacity (255 opaque, 0 invisible). To turn #rrggbb into the integer: take the red, green and blue values as numbers from 0 to 255, then compute a × 16777216 + b × 65536 + g × 256 + r.
| Colour | a, b, g, r | Integer |
|---|---|---|
white #ffffff | 255, 255, 255, 255 | 4294967295 |
black #000000 | 255, 0, 0, 0 | 4278190080 |
orange #ff5a1f | 255, 31, 90, 255 | 4280244991 |
light blue #aadaff | 255, 255, 218, 170 | 4294957738 |
pink #ff69b4 | 255, 180, 105, 255 | 4290013695 |
red at half opacity #ff0000 | 128, 0, 0, 255 | 2147483903 |
Note the order: blue comes before red, the reverse of #rrggbb. Studio shows colours as hex; the file stores only integers.
Easings
easing_type | Name | easing_type | Name | |
|---|---|---|---|---|
| 1 | linear | 12 | easeOutQuart | |
| 2 | easeInSine | 13 | easeInOutQuart | |
| 3 | easeOutSine | 14 | easeInQuint | |
| 4 | easeInOutSine | 15 | easeOutQuint | |
| 5 | easeInQuad | 16 | easeInOutQuint | |
| 6 | easeOutQuad | 17 | easeInExpo | |
| 7 | easeInOutQuad | 18 | easeOutExpo | |
| 8 | easeInCubic | 19 | easeInOutExpo | |
| 9 | easeOutCubic | 20 | easeInCirc | |
| 10 | easeInOutCubic | 21 | easeOutCirc | |
| 11 | easeInQuart | 22 | easeInOutCirc |
Filters
"filter": {
"condition": "all",
"expression": [
{ "key": "category", "condition": "in", "values": ["culture", "food"] },
{ "key": "name", "condition": "has" }
]
}
The top condition is all, any or none. Each entry's condition is one of has, hasnot, in, notin, ==, !=, >, <, >=, <=; has and hasnot take no values. On the tutorial's sample file, bucharest-places.geojson, this filter keeps four of the eight places: the three culture places and the food market, which all have a name.
Every value of your own data is stored as text, and text comparisons ignore upper and lower case. >, <, >= and <= compare whole numbers and work only on the map data's number fields, Lanes count on roads and Height on buildings. On text, including every field of your own data, they let every feature through. The fields of the map data are listed in Map data sources.
Text
Layers that draw map data, such as city labels (symbol), road names (street), area labels (areaname), country labels (country) and POIs, draw their text by themselves: leave label_text out. None of the official templates sets it on these layers. Use label_text on layers that draw your own data sources, to choose what they show.
A symbol layer's text is an expression, stored as a JSON string in label_text.label_expression. Its pieces are { "text": "..." }, { "number": 20 }, { "get_v": "name" } for a value of the feature, and { "concat": [ ... ] } or { "*": [ ... ] } over their arguments. For example, the label concat(|name|, " (", |category|, ")") in Studio, which reads "Obor Market (food)", is stored as:
"label_text": {
"label_expression": "{\"concat\":[{\"get_v\":\"name\"},{\"text\":\" (\"},{\"get_v\":\"category\"},{\"text\":\")\"}]}"
}
The result of * is shown with two decimals: product(|rating|, 20) on a rating of 4.6 reads 92.00.
Images
Each image in icons_section.icons is { "id": "bike.png", "value": "data:image/png;base64,..." }: a unique name and the image as a PNG, JPEG or SVG data URI. Images can be up to 400 KB each and 4 MB in total.
A layer uses an image through the icon property, whose custom_icon holds the image's id; "" means no icon. POI layers draw their category's icon from metaCategoryID instead.
Fonts
fonts_section holds the style's own font for its labels. It comes after icons_section and is left out when the style uses the map's built-in font:
"fonts_section": {
"regular": "data:font/ttf;base64,AAEAAAAQAQAABAAA...",
"bold": "data:font/ttf;base64,AAEAAAAQAQAABAAA..."
}
| Key | What it holds |
|---|---|
regular | The face for layers whose text-style is normal (0). |
bold | The face for layers whose text-style is bold (1). |
italic | The face for layers whose text-style is italic (2). |
Each key is optional and works on its own: a face that is missing is drawn in the map's built-in font.
Each value is a whole TTF or OTF file as a base64 data URI, data:font/ttf;base64,... or data:font/otf;base64,...; there is no size limit, but the file is part of every save and download of the style, and the engine reads it whole when the style is opened. WOFF and WOFF2 web fonts and font collections (.ttc) are not read. There is no font per layer: every label uses this font, and a character the font does not have is drawn in the built-in font.
The font is part of the file, so it is in the .style file too. Font data cannot be written by hand or generated: a file Studio cannot read as a font is left out when the style is opened. Add fonts in Settings › Typography (see The style's font).
A complete small style
A whole style with nine layers: city labels, motorway names, motorways, residential streets, parks, water, sea, land and the background. Its colours and sizes come from the Basic 1 template. Download it as example-small-style.json.
{
"sources_list": { "sources": [] },
"map_schemes_section": {
"COMMENT": "Define all available map schemes.",
"map_schemes": [{
"id": 1,
"name": "Small example",
"groups_section": { "groups": [
{"id": 1, "name": "Labels"},
{"id": 2, "name": "Roads"},
{"id": 3, "name": "Areas"}
] },
"definition": { "layers": [
{ "COMMENT": "City labels (over 500k)", "id": 9, "type": "symbol", "source": "map", "sourcedata_id": 0, "minzoom": 3, "maxzoom": 13, "group_id": 1, "paint": {
"text-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4281807665}], "easing_type": 1}},
"text-size": {"text_size_stops": {"text_size_stop": [{"level_id": 4, "size": 3}, {"level_id": 13, "size": 6}], "easing_type": 1}},
"text-outline-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4294967295}], "easing_type": 1}},
"text-outline-size": {"text_size_stops": {"text_size_stop": [{"level_id": 0, "size": 1}], "easing_type": 1}}
} },
{ "COMMENT": "Motorway names", "id": 8, "type": "street", "source": "map", "sourcedata_id": 0, "minzoom": 11, "maxzoom": 17, "group_id": 1, "paint": {
"text-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4280829573}], "easing_type": 1}},
"text-size": {"text_size_stops": {"text_size_stop": [{"level_id": 12, "size": 2.5}, {"level_id": 17, "size": 6}], "easing_type": 1}},
"text-outline-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4294967295}], "easing_type": 1}},
"text-outline-size": {"text_size_stops": {"text_size_stop": [{"level_id": 0, "size": 1}], "easing_type": 1}}
} },
{ "COMMENT": "Motorway", "id": 7, "type": "line", "source": "map", "sourcedata_id": 0, "minzoom": 4, "maxzoom": 17, "group_id": 2, "paint": {
"line-inner-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4288801791}], "easing_type": 1}},
"line-outer-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4284862454}], "easing_type": 1}},
"line-width": {"line_width_stops": {"line_width_stop": [{"level_id": 4, "width": 7}, {"level_id": 10, "width": 3}, {"level_id": 13, "width": 2.5}, {"level_id": 17, "width": 5}], "easing_type": 1}},
"line-outline-width": {"line_width_stops": {"line_width_stop": [{"level_id": 4, "width": 1}, {"level_id": 11, "width": 0.5}, {"level_id": 16, "width": 0.1}], "easing_type": 1}}
} },
{ "COMMENT": "Residential", "id": 6, "type": "line", "source": "map", "sourcedata_id": 6, "minzoom": 11, "maxzoom": 17, "group_id": 2, "paint": {
"line-inner-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4294967295}], "easing_type": 1}},
"line-outer-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4292662998}], "easing_type": 1}},
"line-width": {"line_width_stops": {"line_width_stop": [{"level_id": 10, "width": 1}, {"level_id": 13, "width": 1.75}, {"level_id": 15, "width": 1.5}], "easing_type": 1}},
"line-outline-width": {"line_width_stops": {"line_width_stop": [{"level_id": 0, "width": 0.15}], "easing_type": 1}}
} },
{ "COMMENT": "Green Park", "id": 5, "type": "polygon", "source": "map", "sourcedata_id": 23, "minzoom": 0, "maxzoom": 17, "group_id": 3, "paint": {
"fill-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4291160517}], "easing_type": 1}}
} },
{ "COMMENT": "Water", "id": 4, "type": "polygon", "source": "map", "sourcedata_id": 9, "minzoom": 0, "maxzoom": 17, "group_id": 3, "paint": {
"fill-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4294957738}], "easing_type": 1}}
} },
{ "COMMENT": "Water Sea", "id": 3, "type": "polygon", "source": "map", "sourcedata_id": 94, "minzoom": 0, "maxzoom": 17, "group_id": 3, "paint": {
"fill-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4294957738}], "easing_type": 1}}
} },
{ "COMMENT": "Land", "id": 2, "type": "polygon", "source": "map", "sourcedata_id": 0, "minzoom": 0, "maxzoom": 17, "group_id": 3, "paint": {
"fill-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4294638072}], "easing_type": 1}}
} },
{ "COMMENT": "Background", "id": 1, "type": "background", "sourcedata_id": 0, "paint": {
"fill-color": {"color_stops": {"color_stop": [{"level_id": 0, "color": 4294957738}], "easing_type": 1}}
} }
] }
}]
},
"icons_section": { "COMMENT": "Define all available icons for schemes.", "icons": [] }
}
For a full style to study or start from, download the Basic 1 template, as the portal offers it: template-basic-1.json (222 layers and 6 images). One value is corrected so that the file passes the schema: the portal's copy stores the text anchor of the Narrow Water Line Label layer as the text "2", and this file has the number 2.
The JSON Schema
The rules above are published as a JSON Schema (draft 2020-12): engine-style.schema.json.
It checks the sections and the single scheme; every layer field and its type; the layer type and source names; every paint property's wrapper, stop array, value key and range (no negative sizes, opacity 0 to 1, zoom 0 to 17, easing_type 1 to 22); which properties each layer type takes; colours as integers; filters, text, images and the font's faces. Each property also carries its help text from the Properties column. Fields the engine reads but Studio does not write are accepted and marked "x-written-by-studio": false.
A few rules link one part of the file to another, which a schema cannot express. They are listed in the schema under x-checks-not-in-schema: layer ids go down from each layer to the next and end at 1; every group_id and parent_id names an existing group; every custom_icon names an image; minzoom is not above maxzoom; each fonts_section value is a real TTF or OTF file.
To check a file, save the schema next to it and run, with Python and jsonschema 4.18 or later (pip install "jsonschema>=4.18"):
python3 -c "import json, jsonschema; jsonschema.validate(json.load(open('my-style.json')), json.load(open('engine-style.schema.json')))"
No output means the file passed; otherwise the first problem is printed with its place in the file.
See also: Map data sources · Layer types reference · Download and use in your app