Custom indoor mapping
All Mapbox renderers support indoor mapping: the data schema, layer types, and floor-switching system work in Mapbox GL JS, the Maps SDK for Android, and the Maps SDK for iOS.
The Mapbox Standard style uses this same indoor mapping system to display curated floor plans for selected venues. When you enable showIndoor on the Standard style, you are enabling Mapbox's own implementation of indoor mapping, backed by the mapbox.indoor-v3 tileset.
The same underlying system is available to any developer: bring your own floor plan data in the required format and the renderer will handle it the same way.
This guide covers the data schema, map configuration, styling, floor-switching UI, and everything else needed to build your own indoor maps.
Prerequisites
- Mapbox GL JS v3.4.0 or later —
IndoorControland theis-active-floorexpression were introduced in this release. Earlier versions can display indoor layers using manualfloor_idfilters, but do not have the built-in floor-switching control. - Maps SDK for Android v11.25 or later or Maps SDK for iOS v11.25 or later — custom indoor mapping support was introduced in this release.
- Floor plan geometry for your venue in GeoJSON format, or a vector tileset.
Data model
Indoor maps follow a strict four-level hierarchy:
Facility (the overall campus or site)
└── Structure (an individual building)
└── Floor (a level within the building)
└── Room (polygon, point, or linear features on a floor: rooms,
corridors, walls, connectors, doors, desks, POIs, etc.)
Each level references its parent using string ID fields. The diagram below shows which fields create those links:
┌──────────────────────────────────────┐
│ Facility │
│ type: "facility" │
│ id: "facility-abc" ────────────────┼──┐
└──────────────────────────────────────┘ │
│
┌──────────────────────────────────────┐ │
│ Structure │ │
│ type: "structure" │ │
│ facility_id: "facility-abc" ◄──────┼──┘
│ id: "structure-abc" ───────────────┼──┐
└──────────────────────────────────────┘ │
│
┌──────────────────────────────────────┐ │
│ Floor │ │
│ type: "floor" │ │
│ structure_ids: "structure-abc" ◄───┼──┘
│ id: "floor-abc" ───────────────────┼──┐
└──────────────────────────────────────┘ │
│
┌──────────────────────────────────────┐ │
│ Room │ │
│ type: "room" │ │
│ floor_id: "floor-abc" ◄────────────┼──┘
└──────────────────────────────────────┘
Each feature has a type property in its properties object that identifies its role in the hierarchy. All four feature types can coexist in a single source.
Indoor map data can be served as a GeoJSON source (a FeatureCollection loaded directly into the map) or as a vector tileset. Both work with the same indoor configuration and floor-switching system, as long as the required properties are present on each feature.
The type field in each feature's properties object (for example,
"type": "floor") identifies its role in the indoor hierarchy. This is
separate from the GeoJSON type field ("Feature") or a vector tile layer
name. The indoor system uses the properties.type value regardless of source
format.
Feature types and properties
Facility
The top-level container representing the entire site (an airport, campus, shopping center, etc.).
| Property | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "facility" |
id | string | Yes | Unique identifier, for example "facility-my-building" |
name | string | Yes | Display name of the facility |
Geometry: Polygon representing the outer boundary of the facility.
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[...]]]
},
"properties": {
"type": "facility",
"id": "facility-central-station",
"name": "Central Station"
}
}
Structure
An individual building within a facility. One facility can contain multiple structures (for example, a campus with several buildings, or an airport with multiple concourses treated as separate structures).
| Property | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "structure" |
id | string | Yes | Unique identifier, for example "structure-terminal-a" |
name | string | Yes | Display name of the structure |
facility_id | string | Yes | References the parent facility's id |
center_lon | number | Yes | Longitude of the structure's center point |
center_lat | number | Yes | Latitude of the structure's center point |
Geometry: Polygon representing the building footprint. This polygon is also used by the floor-slab fill-extrusion layer to render a base beneath the indoor map.
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[...]]]
},
"properties": {
"type": "structure",
"id": "structure-terminal-a",
"name": "Terminal A",
"facility_id": "facility-central-station",
"center_lon": -73.9923373,
"center_lat": 40.7278090
}
}
center_lon and center_lat are used by the floor-switching control to
determine which structure the user is now viewing. Make sure they
accurately reflect the geographic center of the building.
Floor
A single level within a structure. The floor selector displays or hides each floor independently. Generally only one floor is visible at a time per structure; in complex cases (such as multi-structure floors or mezzanines), visibility is controlled by connected_floor_ids and conflicted_floor_ids.
| Property | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "floor" |
id | string | Yes | Unique identifier, for example "floor-level-1" |
name | string | Yes | Short label the IndoorControl UI renders as the floor button (for example "1", "B", "G") |
description | string | No | Longer descriptive name the IndoorControl renders as a tooltip or subtitle when that floor is active (for example "First Floor", "Basement") |
structure_ids | string | Yes | The id of the parent structure. To associate a floor with multiple structures, use ;-separated values |
z_index | number | Yes | Controls vertical ordering. Use 0 for ground level, negative values for below-grade floors, and positive values for above-ground floors |
is_default | boolean | No | If true, the map shows this floor when the user first navigates to the building. Only one floor should be default per structure |
conflicted_floor_ids | string | No | ;-separated list of floor ids that cannot be displayed at the same time (typically all other floors in the same structure) |
Geometry: Polygon representing the floor's footprint. This is usually the same polygon as the structure footprint.
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[...]]]
},
"properties": {
"type": "floor",
"id": "floor-level-1",
"name": "1",
"description": "First Floor",
"structure_ids": "structure-terminal-a",
"z_index": 0,
"is_default": true,
"conflicted_floor_ids": "floor-basement;floor-level-2;floor-level-3"
}
}
The renderer does not automatically know which floors are mutually exclusive —
it cannot infer this from structure_ids alone because a floor can span
multiple structures. You must declare conflicts explicitly. The floor-switching
logic uses conflicted_floor_ids to hide the listed floors when a given floor
is selected. Typically, each floor's conflicted_floor_ids should list every
other floor in the same structure, separated by ;. The relationship must be
declared in both directions: if floor A lists floor B, floor B must also list
floor A. Without this, multiple floors may render on top of each other.
Room
Rooms are the atomic features of an indoor map—individual spaces on a floor such as rooms, corridors, walls, connectors (stairs, elevators, escalators), and other interior elements. The class property on each room determines both how it is styled and how it behaves in the rendering pipeline.
| Property | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "room" for all interior features, including walls |
id | string | Yes | Unique identifier, for example "room-floor-level-1-lobby" |
name | string | Yes | Display name (used as a label for non-wall features) |
floor_id | string | Yes | References the parent floor's id |
class | string | Yes | Categorizes the room for styling and layer filtering. See Room classes below |
icon | string | No | Icon image name for connector-type rooms (for example "elevator", "stairs") |
Geometry: Polygon, Point, or LineString. Most room features are polygons (spaces, walls), but the room level also supports point features (doors, desks, POIs) and line features. Wall features use a standard polygon — no special geometry is required. Wall thickness comes from a fill-extrusion-line-width paint expression in the style layer, which extrudes only the polygon edges rather than the filled area.
ID referencing
Features reference their parents using string ID fields. The full reference chain is:
facility.id ◄── structure.facility_id
structure.id ◄── floor.structure_ids
floor.id ◄── room.floor_id
floor.id ◄── floor.conflicted_floor_ids (self-referential, ;-separated)
Naming convention
Use a consistent, hierarchical naming pattern for feature IDs so that IDs stay readable and useful for debugging:
| Level | Pattern | Example |
|---|---|---|
| Facility | facility-{name} | facility-central-station |
| Structure | structure-{name} | structure-terminal-a |
| Floor | floor-{level} | floor-level-1, floor-basement |
| Room | room-{floor-id}-{room-name} | room-floor-level-1-lobby |
| Wall | wall-{floor-id}-{description} | wall-floor-level-1-exterior |
Every feature must have a unique id within its source. Duplicate IDs will
cause unexpected behavior in floor filtering and feature queries.
Map configuration
Style setup
Indoor maps work by layering your data on top of a base style (typically Mapbox Standard) using the style's sources, layers, and the special indoor configuration block. The source can be a GeoJSON source or a vector tileset — both work the same way. Below is an example using a GeoJSON source:
map = new mapboxgl.Map({
container: 'map',
style: {
version: 8,
imports: [
{
id: 'basemap',
url: 'mapbox://styles/mapbox/standard',
config: {
showIndoor: false // disable built-in airport indoor so it doesn't conflict
}
}
],
sources: {
'indoor-data': {
type: 'geojson',
data: indoorFeatureCollection // or use type: 'vector' with a tileset URL
},
'clip-area': {
type: 'geojson',
data: buildingFootprintPolygon // polygon covering the building footprint
}
},
layers: [
// clip layer first, then indoor layers — see Layer rendering order below
],
indoor: {
'structure-terminal-a': {
sourceId: 'indoor-data'
}
}
}
});
The indoor configuration block
The indoor property on the style object is what registers your data with Mapbox's indoor system. Each key is a structure id from your data, and the value specifies which source contains the data for that structure.
{
"indoor": {
"structure-terminal-a": {
"sourceId": "indoor-data"
}
}
}
Registering a structure in the indoor block enables two things:
IndoorControlfloor selector UI — the floor switcher control will appear automatically when the map is zoomed into the registered structure above the minimum zoom level.is-active-floorexpression — a special expression becomes available for use in layer filters to show only the active floor's features.
A single source layer can contain multiple structure features — there is no need to register each structure as a separate entry in the indoor block. Register structures separately only if their metadata comes from different sources. For example, the mapbox.indoor-v3 tileset's indoor_structure_metadata layer contains all its structures as individual features within one layer.
The is-active-floor expression
The is-active-floor expression evaluates to true when a feature's floor_id matches the active floor. Use it as a filter on all layers that should respond to floor switching:
["is-active-floor", ["get", "floor_id"]]
Apply this filter to every room layer, label layer, and connector layer in your indoor map. Without it, all floors will be rendered simultaneously.
map.addLayer({
id: 'indoor-room-fills',
type: 'fill',
source: 'indoor-data',
filter: [
'all',
['==', ['get', 'type'], 'room'],
['!=', ['get', 'class'], 'wall'],
['is-active-floor', ['get', 'floor_id']]
],
paint: {
'fill-color': '#e8e8e8',
'fill-opacity': 0.9
}
});
Styling indoor features
Indoor features are regular map features — the renderer treats them no differently from any other source. You can style them with fill, fill-extrusion, line, circle, or symbol layers using the same expressions and paint properties you would use anywhere else.
A common pattern is to add a class property to each room feature and use a match expression to color by type. Always combine room layers with is-active-floor so they respond to floor switching:
map.addLayer({
id: 'indoor-rooms',
type: 'fill',
source: 'indoor-data',
filter: [
'all',
['==', ['get', 'type'], 'room'],
['!=', ['get', 'class'], 'wall'],
['is-active-floor', ['get', 'floor_id']]
],
paint: {
'fill-color': [
'match', ['get', 'class'],
'corridor', '#cce4f7',
'restaurants', '#fde8c8',
'retail', '#fcd9e0',
'connector', '#e8d5f5',
'inaccessible', '#d6d6d6',
'#e8e8e8' // default
],
'fill-opacity': 0.9
}
});
For walls, use fill-extrusion with a fill-extrusion-line-width expression to render them as 3D geometry. The fill-extrusion-line-width property extrudes only the polygon's edges (rather than its filled area), creating the visual appearance of wall thickness from a standard room polygon — no special wall geometry is needed:
map.addLayer({
id: 'indoor-walls',
type: 'fill-extrusion',
source: 'indoor-data',
filter: [
'all',
['==', ['get', 'type'], 'room'],
['==', ['get', 'class'], 'wall'],
['is-active-floor', ['get', 'floor_id']]
],
paint: {
'fill-extrusion-color': '#aaaaaa',
'fill-extrusion-height': 3,
'fill-extrusion-line-width': 0.3, // wall thickness in meters
'fill-extrusion-opacity': 0.8
}
});
The property names and values are entirely up to you — class is one convention. Use any property structure that fits your data model, as long as your style layers reference it consistently.
Use fill-extrusion-height to give walls a realistic three-dimensional
appearance. A typical ceiling height is 3–4 meters. A floor slab extrusion
should use a small height (0.1–0.3 meters) to serve as a base plate without
visually interfering with the room fills above it.
Clip layer: hiding base style 3D buildings
When overlaying indoor data on the Mapbox Standard style (or any style that renders 3D buildings), the base style's 3D building models will overlap and visually conflict with your indoor floor plan. To prevent this, add a clip layer that removes the base style's 3D models and symbols within your building's footprint.
How it works
Add a layer of type "clip" that references a polygon covering your building's footprint, and specify which base-style layer types to suppress. The clip layer must be the first layer in your layers array, before any indoor layers.
If your facility feature's polygon already covers the building footprint, you can point the clip layer directly at your indoor source and filter to the facility feature — no separate source needed:
map.addLayer({
id: 'building-clip',
type: 'clip',
source: 'indoor-data',
filter: ['==', ['get', 'type'], 'facility'],
layout: {
'clip-layer-types': ['model', 'symbol']
}
});
If you need a clip region that doesn't correspond to any existing feature in your data (for example, a slightly enlarged footprint), add a dedicated source:
map.addSource('clip-area', {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [[
[-73.9924377, 40.7276126],
[-73.9923273, 40.7277395],
[-73.9922519, 40.7277016],
[-73.9923623, 40.7275753],
[-73.9924377, 40.7276126]
]]
}
}
});
map.addLayer({
id: 'building-clip',
type: 'clip',
source: 'clip-area',
layout: {
'clip-layer-types': ['model', 'symbol']
}
});
The Standard style renders building labels and POI icons as symbol layers. If
you only clip model layers, text labels from the base style may still appear
floating over your indoor floor plan. Clipping both model and symbol gives
you a clean canvas inside your building's footprint.
Make the clip polygon slightly larger than the building's actual footprint to avoid edge artifacts where the 3D building model bleeds through at the perimeter. A margin of a few meters is usually enough.
Layer rendering order
Add indoor layers to your map in the following order (bottom to top). This order ensures correct visual stacking and prevents artifacts:
| Order | Layer ID (example) | Layer type | What it renders | Filter |
|---|---|---|---|---|
| 1 | building-clip | clip | Hides base style 3D buildings/symbols inside footprint | (no filter — applies to the clip source area) |
| 2 | indoor-floor-slab | fill-extrusion | Thin 3D base slab under the floor plan | type === "structure" |
| 3 | indoor-room-fills | fill | Colored fill for all non-wall rooms | type === "room", class !== "wall", active floor |
| 4 | indoor-walls | fill-extrusion | 3D extruded walls (floor to ceiling height) | type === "room", class === "wall", active floor |
| 5 | indoor-room-labels | symbol | Text labels for rooms and spaces | type === "room", class !== "wall", active floor |
| 6 | indoor-connectors | symbol | Icons for stairs, elevators, escalators | type === "room", class === "connector", active floor |
The fill-extrusion floor slab (layer 2) uses features with type === "structure" — not "floor" — because the structure polygon defines the full
building footprint that should be visible regardless of which floor is
selected. This prevents a gap in the map surface when switching floors.
Floor switching UI
IndoorControl in Mapbox GL JS
Mapbox GL JS includes a built-in IndoorControl that automatically appears when indoor data is in view above the minimum zoom level. After registering your structure in the indoor configuration block, add the control to the map:
const indoorControl = new mapboxgl.IndoorControl();
map.addControl(indoorControl, 'bottom-right');
The control displays the available floors for the structure in view, ordered by z_index, and highlights the active floor. Clicking a floor updates the active floor and triggers re-filtering of all layers using is-active-floor.
The IndoorControl appears and disappears automatically based on the map's
current view. It becomes visible when a registered structure is within the
viewport and the map zoom level is at or above zoom 16, and hides when the
user pans or zooms away. The control uses the floor name property as the
button label, and the optional description property as a tooltip or subtitle
when that floor is selected.
Floor switching on mobile and other renderers
The Maps SDKs for Android and iOS can render indoor map layers because they support the same standard layer types (fill, fill-extrusion, symbol, clip). You may need to implement your own floor-switching UI for mobile applications:
The general approach for a custom floor switcher:
- Identify the available floors by querying the source or reading your data to find all
floorfeatures for the current structure. - Build a floor selector UI (a tab bar, a vertical list, a bottom sheet, etc.) using the floor
nameanddescriptionproperties. - Track the active floor in application state and update layer filters when the user makes a selection.
- Update the layer filters when the user selects a different floor, replacing
is-active-floorwith an explicitfloor_idcomparison:
// Build the list of available floors for a structure
function getFloorsForStructure(structureId, sourceFeatures) {
return sourceFeatures
.filter(f =>
f.properties.type === 'floor' &&
f.properties.structure_ids.split(';').includes(structureId)
)
.sort((a, b) => a.properties.z_index - b.properties.z_index);
}
// Track current floor in state
let currentFloorId = null;
// Call this when the user selects a floor from your UI
function selectFloor(floorId) {
currentFloorId = floorId;
const floorFilter = ['==', ['get', 'floor_id'], floorId];
map.setFilter('indoor-room-fills', ['all',
['==', ['get', 'type'], 'room'],
['!=', ['get', 'class'], 'wall'],
floorFilter
]);
map.setFilter('indoor-walls', ['all',
['==', ['get', 'type'], 'room'],
['==', ['get', 'class'], 'wall'],
floorFilter
]);
map.setFilter('indoor-room-labels', ['all',
['==', ['get', 'type'], 'room'],
['!=', ['get', 'class'], 'wall'],
floorFilter
]);
map.setFilter('indoor-connectors', ['all',
['==', ['get', 'type'], 'room'],
['==', ['get', 'class'], 'connector'],
floorFilter
]);
}
is-active-floor is a Mapbox GL JS-specific expression that hooks into the
IndoorControl's state. On mobile SDKs or in environments without
IndoorControl, use an explicit ['==', ['get', 'floor_id'], currentFloorId]
filter instead, updating it programmatically when the user selects a floor.
Data example
Below is a minimal but complete example for a single-structure, two-floor facility. It includes one facility, one structure, two floors, and a selection of rooms for the ground floor: a corridor, a cafe, an elevator connector, and a perimeter wall. The same feature structure and properties apply when the data is stored in a vector tileset.
Note: // comments below are included for clarity and are not valid JSON — remove them before using this data.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-73.9925, 40.7275],
[-73.9921, 40.7275],
[-73.9921, 40.7279],
[-73.9925, 40.7279],
[-73.9925, 40.7275]
]]
},
"properties": {
"type": "facility",
"id": "facility-example-building",
"name": "Example Building"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
// For a single-building facility, the structure polygon typically
// matches the facility polygon exactly.
"coordinates": [[
[-73.9925, 40.7275],
[-73.9921, 40.7275],
[-73.9921, 40.7279],
[-73.9925, 40.7279],
[-73.9925, 40.7275]
]]
},
"properties": {
"type": "structure",
"id": "structure-example-building",
"name": "Example Building",
"facility_id": "facility-example-building",
"center_lon": -73.9923,
"center_lat": 40.7277
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
// Floor polygons typically match the structure footprint.
// The polygon defines the floor's extent for filtering purposes;
// individual room polygons define the actual spaces within it.
"coordinates": [[
[-73.9925, 40.7275],
[-73.9921, 40.7275],
[-73.9921, 40.7279],
[-73.9925, 40.7279],
[-73.9925, 40.7275]
]]
},
"properties": {
"type": "floor",
"id": "floor-ground",
"name": "G",
"description": "Ground Floor",
"structure_ids": "structure-example-building",
"z_index": 0,
"is_default": true,
// conflicted_floor_ids must list all sibling floors. floor-level-2
// must also list floor-ground in its own conflicted_floor_ids.
"conflicted_floor_ids": "floor-level-2"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-73.9925, 40.7275],
[-73.9921, 40.7275],
[-73.9921, 40.7279],
[-73.9925, 40.7279],
[-73.9925, 40.7275]
]]
},
"properties": {
"type": "floor",
"id": "floor-level-2",
"name": "2",
"description": "Second Floor",
"structure_ids": "structure-example-building",
"z_index": 1,
"conflicted_floor_ids": "floor-ground"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-73.9924, 40.7276],
[-73.9922, 40.7276],
[-73.9922, 40.7278],
[-73.9924, 40.7278],
[-73.9924, 40.7276]
]]
},
"properties": {
"type": "room",
"id": "room-floor-ground-lobby",
"name": "Main Lobby",
"floor_id": "floor-ground",
"class": "corridor"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-73.9924, 40.7278],
[-73.9922, 40.7278],
[-73.9922, 40.7279],
[-73.9924, 40.7279],
[-73.9924, 40.7278]
]]
},
"properties": {
"type": "room",
"id": "room-floor-ground-cafe",
"name": "Café",
"floor_id": "floor-ground",
"class": "restaurants"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
// Connectors are small polygons representing the physical location
// of the stairwell, elevator shaft, or escalator on the floor plan.
"coordinates": [[
[-73.99215, 40.72755],
[-73.99210, 40.72755],
[-73.99210, 40.72765],
[-73.99215, 40.72765],
[-73.99215, 40.72755]
]]
},
"properties": {
"type": "room",
"id": "room-floor-ground-elevator",
"name": "Elevator",
"floor_id": "floor-ground",
"class": "connector",
"icon": "elevator"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
// Wall features use a standard polygon — the same geometry as any room.
// Wall thickness is produced by fill-extrusion-line-width in the style
// layer, which extrudes only the polygon edges rather than the filled area.
"coordinates": [[
[-73.9925, 40.7275],
[-73.9921, 40.7275],
[-73.9921, 40.7279],
[-73.9925, 40.7279],
[-73.9925, 40.7275]
]]
},
"properties": {
// Walls use type "room" with class "wall" — there is no separate
// type for walls. The class distinguishes them from other rooms.
"type": "room",
"id": "wall-floor-ground-exterior",
"name": "Exterior Wall",
"floor_id": "floor-ground",
"class": "wall"
}
}
]
}
Data schema reference
The custom indoor map data schema is aligned with the layers in the mapbox.indoor-v3 tileset that powers the Standard style's built-in indoor maps. The key differences are:
| Aspect | Mapbox indoor-v3 tileset | Custom indoor data |
|---|---|---|
| Source type | Vector tileset (mapbox://mapbox.indoor-v3) | GeoJSON or vector tileset |
| Feature separation | Separate layers per type (indoor_structure, indoor_floor, indoor_floorplan, etc.) | Features differentiated by the type property within one or more geojson sources or one or more source layers in a vector tileset |
| Maintained by | Mapbox team | You |
| Coverage | Major airports | Any facility you model |
Troubleshooting
All floors render at the same time
The most common cause is a missing or incorrect is-active-floor filter on one or more layers. Verify that every fill, fill-extrusion, and symbol layer for room features includes ['is-active-floor', ['get', 'floor_id']] in its filter. Also check that conflicted_floor_ids is fully populated for every floor and that each floor lists all its siblings — if any floor's conflict list is incomplete, multiple floors can be active simultaneously.
3D building model bleeds through the floor plan
The clip layer is either missing or was added after the indoor layers. The clip layer must be the first layer added to the map. Also confirm that the clip polygon covers the full building footprint — if it is smaller than the structure polygon, the building model will appear at the edges.
The floor selector (IndoorControl) doesn't appear
Check the following in order:
- The structure's
idin theindoorconfiguration block exactly matches theidin the structure'sproperties. - The
sourceIdin theindoorconfiguration block matches the key used when the source was added. - The map zoom level is at or above 16 and the structure is within the viewport.
center_lon/center_laton the structure feature are correct — if they place the center outside the viewport, the control will not appear.
Rooms show on the wrong floor, or not at all
Check that floor_id values on room features exactly match the id values on floor features — casing and punctuation matter. A mismatch means the is-active-floor expression (or a manual floor_id filter) will never check as true for those rooms.
The floor selector shows floors in the wrong order
The IndoorControl orders floors by z_index. Set ground level to 0, above-grade floors to positive values, and below-grade floors to negative values. The UI sorts floors ascending by z_index.
Related resources
Reference documentation for the indoor-v3 tileset that powers indoor maps in the Mapbox Standard style.
Learn how Mapbox styles work and how to customize map appearance.
Learn how to use expressions to create data-driven styles and filters.