Load Mapbox Vector Tiles in CesiumJS
This tutorial demonstrates how to stream Mapbox Vector Tiles directly from a tile server into CesiumJS. Provide a standard {z}/{x}/{y} URL template and CesiumJS will request and decode the tiles in the browser as the application runs.
Because the tiles are loaded directly from the source service, no offline conversion or preprocessing pipeline is required.
Prerequisites
- A CesiumJS build that includes MVTDataProvider. This tutorial uses CesiumJS 1.145.
- A Cesium ion account and access token. Sign up for a free Cesium ion account if you don't already have one.
- The URL of an MVT tile service that serves
.mvtor.pbftiles behind a{z}/{x}/{y}path. - Know how to set up a basic CesiumJS application. Check out our CesiumJS Quickstart guide if you have not built one before.
Why vector tiles
Mapbox Vector Tiles are widely used to serve vector data in web mapping applications. Unlike raster tiles, which contain pre-rendered pixels, MVT tiles encode vector geometry and feature attributes using Google Protocol Buffers (PBF).
Because the geometry remains vector-based, features can be restyled at runtime, while the attributes encoded in each tile remain available for styling and picking. MVTDataProvider requests and decodes tiles directly from an MVT service, so an endpoint already used by a 2D map can also be reused in a CesiumJS scene without an intermediate conversion step.
Begin by creating a standard CesiumJS viewer. No MVT specific configuration is required at this stage.
Cesium.Ion.defaultAccessToken = "your_ion_token";
const viewer = new Cesium.Viewer("cesiumContainer", {
terrain: Cesium.Terrain.fromWorldTerrain(),
});
Create an MVTDataProvider from the tile service URL and add it to the scene.
MVTDataProvider currently builds its complete runtime tile hierarchy when it is initialized, so provide the source zoom range and geographic extent when creating the provider. The example below uses zoom levels 6 through 14 and an extent covering Switzerland.
const provider = await Cesium.MVTDataProvider.fromUrl(
"https://example.com/tiles/{z}/{x}/{y}.pbf",
{
minZoom: 6,
maxZoom: 14,
extent: Cesium.Rectangle.fromDegrees(5.9, 45.8, 10.5, 47.8),
},
);
viewer.scene.primitives.add(provider);
await viewer.zoomTo(provider.tileset);
The tile hierarchy is created up front, while tile contents are requested and decoded on demand as the camera moves. The generated content is rendered through the provider’s underlying 3D Tiles tileset.
URL template requirements
The URL template must include the {z}, {x}, and {y} placeholders. These values follow the standard Web Mercator XYZ tile scheme used by OpenStreetMap, Mapbox, and many other tile services.
The file extension is not significant. URLs ending in .mvt, .pbf, or no extension are all supported, provided that the server returns a valid MVT binary response.
Authentication and custom request options
When a service requires an API key, custom query parameter, or request header, pass a Cesium.Resource instead of a URL string.
const provider = await Cesium.MVTDataProvider.fromUrl(
new Cesium.Resource({
url: "https://example.com/tiles/{z}/{x}/{y}.pbf",
queryParameters: {
api_key: "your_key",
},
}),
{
minZoom: 6,
maxZoom: 14,
extent: Cesium.Rectangle.fromDegrees(5.9, 45.8, 10.5, 47.8),
},
);

The values passed above should match the coverage published by the tile service. minZoom and maxZoom define the available zoom range, while extent limits the geographic area included in the generated hierarchy.
When no coverage options are provided, the hierarchy covers the entire globe from zoom level 0 through zoom level 14. This produces roughly 358 million tile nodes and can exhaust browser memory before rendering begins.
The following sections explain how to choose these values.
Configure the zoom range
Set minZoom and maxZoom to match the levels published by the tile service.
The size of the generated hierarchy is driven mainly by the number of tiles that intersect extent at maxZoom. Each additional zoom level can increase the number of tiles at the deepest level by up to four times. Raising minZoom removes only the relatively small number of coarse tiles near the top of the hierarchy, so it does not significantly reduce memory usage.
If maxZoom is higher than the deepest level published by the service, CesiumJS may refine into levels with no source tiles and request tiles that do not exist. If it is too low, the layer stops refining before reaching the full detail available from the source.
With the current implementation, avoid using a high maxZoom with a global extent. A global hierarchy through zoom level 8 already contains about 87,000 nodes. Use a smaller regional extent for higher zoom levels.
Configure the geographic extent
The extent option limits the generated tile hierarchy to the geographic footprint of the dataset. Use the smallest rectangle that contains all source data.
An extent that is too broad creates unnecessary tile nodes during initialization. An extent that is too narrow prevents data outside the rectangle from loading.
When the bounds are expressed in longitude and latitude, create the rectangle with:
Cesium.Rectangle.fromDegrees(west, south, east, north);
Note: Missing tiles: HTTP 404 and 204 responses are treated as empty tiles rather than loading errors. This allows sparse datasets to be hosted without generating placeholder files for areas that contain no features.

Each rendered feature retains the attributes stored in its MVT layer. These attributes can be referenced directly through Cesium3DTileStyle.
The following example styles roads according to their class property:
provider.tileset.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
["${class} === 'motorway'", "color('#ff6b35')"],
["${class} === 'primary'", "color('#f7c59f')"],
["true", "color('#cccccc', 0.6)"],
],
},
lineWidth: "${class} === 'motorway' ? 4.0 : 2.0",
});
Property names are preserved when the tile is decoded. Expressions such as ${class}, ${name}, and ${population} therefore refer directly to MVT attributes with those names.
Property names are case-sensitive and must match the names in the source tiles exactly.
| Geometry type | Supported style properties |
|---|---|
| Points | color, show, pointSize, pointOutlineWidth, pointOutlineColor |
| Lines | color, show, lineWidth |
| Polygons | color, show |
The show property can also be used to control feature visibility based on an attribute.
provider.tileset.style = new Cesium.Cesium3DTileStyle({
show: "${population} > 100000",
});
This example displays only features whose population property is greater than 100,000.

MVT features use the same picking workflow as other 3D Tiles content. Use Scene.pick to retrieve a feature at the selected screen position, then read its properties.
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((movement) => {
const picked = viewer.scene.pick(movement.position);
if (!Cesium.defined(picked)) {
return;
}
for (const id of picked.getPropertyIds()) {
console.log(`${id}: ${picked.getProperty(id)}`);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
Use a source property as the feature ID
When the tiles contain a stable identifier, specify that property through featureIdProperty.
const provider = await Cesium.MVTDataProvider.fromUrl(url, {
minZoom: 6,
maxZoom: 14,
extent: Cesium.Rectangle.fromDegrees(5.9, 45.8, 10.5, 47.8),
featureIdProperty: "osm_id",
});
This allows the application to associate representations of the same real-world feature across tile boundaries or zoom levels.
For example, a road that crosses several tiles may have a separate rendered representation in each tile. A shared identifier such as osm_id allows the application to recognize that those representations belong to the same source feature.
The provider behaves like any other primitive in the scene.
MVTDataProvider implements the primitive interface, so it is added to and removed from scene.primitives directly and behaves like any other primitive.
Hide the layer without unloading it:
provider.show = false;
Show it again:
provider.show = true;
Remove the provider and release its scene resources:
viewer.scene.primitives.remove(provider);
The generated Cesium3DTileset is available through provider.tileset. Use it when you need to access tileset-level functionality, such as tile events or runtime statistics.
provider.tileset.tileLoad.addEventListener((tile) => {
console.log("Loaded tile:", tile);
});
Optional: Drape MVT onto terrain and 3D Tiles
By default, an MVT layer is rendered on the surface of the ellipsoid. It does not automatically follow terrain or 3D Tiles, so features may sink below the terrain or appear above the surrounding surface. To drape the layer onto surfaces in the scene, provide both heightReference and scene.
The viewer created in Step 1 includes world terrain, so the difference is visible immediately.
const provider = await Cesium.MVTDataProvider.fromUrl(
"https://example.com/tiles/{z}/{x}/{y}.pbf",
{
minZoom: 6,
maxZoom: 14,
extent: Cesium.Rectangle.fromDegrees(5.9, 45.8, 10.5, 47.8),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
scene: viewer.scene,
},
);
Both options are required for clamping. The scene provides access to the surfaces that receive the draped geometry, so heightReference has no effect without it.
Draping requires CesiumJS 1.145 or later. Please check vector draping and styling tutorial for more information.
Troubleshooting
The page freezes or runs out of memory on load
MVTDataProvider currently creates the complete runtime tile hierarchy before rendering begins. A high maxZoom over a broad extent can allocate too many tile nodes during initialization.
Reduce maxZoom, use a tighter extent, or do both. Raising minZoom alone does not significantly reduce the size of the hierarchy. Lazy creation of the tile hierarchy is tracked in issue #13535.
No data appears
Open the browser’s network panel and inspect the tile requests.
If the requests return 404, verify that:
- The URL template is correct.
- The service uses the expected zoom levels.
minZoomandmaxZoomoverlap the levels published by the service.
If no requests are sent, confirm that the camera is inside the configured extent.
Requests are blocked by the browser
The tile server must provide Cross-Origin Resource Sharing headers that allow requests from the application’s origin.
CORS configuration must be updated on the tile server or through a proxy. It cannot be resolved from CesiumJS alone.
Data appears at low zoom levels and then disappears
Verify that maxZoom does not exceed the highest level published by the tile service. Otherwise, CesiumJS may refine beyond the last level containing valid tiles.
All features use the same color
Confirm that a style has been assigned to provider.tileset.style.
If a style is present, verify that the property names in the expressions match the attributes in the MVT data. Pick a feature and print getPropertyIds() to inspect the available names.
Next steps
Try the example in Sandcastle, then replace the sample URL with your own tile service. From there, you can adjust the provider’s zoom range and extent, add styling based on your feature attributes, and connect picking to your application’s interaction logic. Please feel free to share your work on the Community Forum or tag @Cesium on LinkedIn.
Many MVT services also provide a Mapbox GL style JSON. MVTDataProvider does not currently apply those styles automatically, so styles need to be recreated with Cesium3DTileStyle. Direct support for existing Mapbox styles is an area we are continuing to explore.