> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/help/ja/llms.txt)

# Custom indoor mapping

All Mapbox renderers support indoor mapping: the data schema, layer types, and floor-switching system work in [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/), the [Maps SDK for Android](https://docs.mapbox.com/android/maps/guides/), and the [Maps SDK for iOS](https://docs.mapbox.com/ios/maps/guides/).

The [Mapbox Standard style](https://docs.mapbox.com/map-styles/standard/) 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`](https://docs.mapbox.com/data/tilesets/reference/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** — `IndoorControl` and the `is-active-floor` expression were introduced in this release. Earlier versions can display indoor layers using manual `floor_id` filters, 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.

> **Note**
> 
> 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.

```json
{
  "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.

```json
{
  "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
  }
}
```

> **Note**
> 
> `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 `id`s 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.

```json
{
  "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"
  }
}
```

> **Note: conflicted_floor_ids**
> 
> 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` |

> **Note: IDs must be globally unique**
> 
> 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:

```js
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.

```json
{
  "indoor": {
    "structure-terminal-a": {
      "sourceId": "indoor-data"
    }
  }
}
```

Registering a structure in the `indoor` block enables two things:

1.  **`IndoorControl` floor selector UI** — the floor switcher control will appear automatically when the map is zoomed into the registered structure above the minimum zoom level.
2.  **`is-active-floor` expression** — a special expression becomes available for use in layer filters to show only the active floor's features.

> **Note: Multiple structures**
> 
> 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:

```json
["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.

```js
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:

```js
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:

```js
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.

> **Note**
> 
> 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:

```js
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:

```js
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']
  }
});
```

> **Note: Why clip symbols too?**
> 
> 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.

> **Note: Clip polygon size**
> 
> 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 |

> **Note**
> 
> 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`](https://docs.mapbox.com/mapbox-gl-js/api/markers/#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:

```js
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`.

> **Note**
> 
> 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](https://docs.mapbox.com/android/maps/guides/) and [iOS](https://docs.mapbox.com/ios/maps/guides/) 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:

1.  **Identify the available floors** by querying the source or reading your data to find all `floor` features for the current structure.
2.  **Build a floor selector UI** (a tab bar, a vertical list, a bottom sheet, etc.) using the floor `name` and `description` properties.
3.  **Track the active floor** in application state and update layer filters when the user makes a selection.
4.  **Update the layer filters** when the user selects a different floor, replacing `is-active-floor` with an explicit `floor_id` comparison:

```js
// 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
  ]);
}
```

> **Note: is-active-floor vs. manual floor filters**
> 
> `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.

```json
{
  "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`](https://docs.mapbox.com/data/tilesets/reference/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:

1.  The structure's `id` in the `indoor` configuration block exactly matches the `id` in the structure's `properties`.
2.  The `sourceId` in the `indoor` configuration block matches the key used when the source was added.
3.  The map zoom level is at or above **16** and the structure is within the viewport.
4.  `center_lon` / `center_lat` on 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

> **Related content (guide): [Mapbox indoor-v3 tileset reference](https://docs.mapbox.com/data/tilesets/reference/mapbox-indoor-v3/)**
> 
> Reference documentation for the indoor-v3 tileset that powers indoor maps in the Mapbox Standard style.

> **Related content (guide): [Map design and styles](https://docs.mapbox.com/help/ja/dive-deeper/map-design/)**
> 
> Learn how Mapbox styles work and how to customize map appearance.

> **Related content (guide): [Expressions reference](https://docs.mapbox.com/style-spec/reference/expressions/)**
> 
> Learn how to use expressions to create data-driven styles and filters.