Switching the map design (tiles)
How the map looks is decided by a single mapDesignType. Pick one of the presets shipped for each provider, or define your own from a style JSON or tile URL. Switching is just an assignment — the map view is never rebuilt.
01 · The design type
Every provider design implements MapDesignTypeInterface (MapDesignTypeProtocol on iOS). Learn these three members and the handling stays identical across providers.
Identifies the design — either the provider native value (e.g. a Google mapType constant) or a string.
The value handed to the provider engine, including the style JSON URL or tile definition.
Rules that switch attribution text by zoom and area. Required when you bring your own tiles.
02 · Presets
Each provider package exposes its standard designs as constants. The sample apps use these lists directly as their options.
iOS · React
React: MapboxDesign
On React: Streets · Outdoors · Light · Dark · SatelliteStreets, plus style JSON from the OsmBright / MapTiler family
iOS / React: ArcGISMapDesign
React: TomTomDesign
React also has StandardLight / Dark · DrivingLight / Dark · MonoLight / Dark
React
React
React
React
Only the rows that differ per platform are annotated; everywhere else the type name and the preset constant names are the same on all three platforms.
03 · Switching
Assign to mapDesignType on the map view state. The state is observed, so the map changes the moment you assign.
// state is MapViewStateInterface<*>; narrow it to the provider's type before assigning
when (state) {
is GoogleMapViewStateInterface ->
state.mapDesignType = GoogleMapDesign.Satellite
is MapLibreViewStateInterface ->
state.mapDesignType = MapLibreDesign.OsmBrightJa
is MapTilerViewStateInterface ->
state.mapDesignType = MapTilerDesign.Toner
}// The initial design is passed when the state is created
@StateObject private var mapLibreState = MapLibreViewState(
mapDesignType: MapLibreDesign.DemoTiles,
cameraPosition: viewModel.initCameraPosition
)
// Switching is just an assignment to a @Published property
mapLibreState.mapDesignType = MapLibreDesign.OsmBrightJaconst [mapViewState, setMapViewState] =
useState<MapViewStateInterface<MapDesignTypeInterface<unknown>> | null>(null);
const handleDesignChange = (designId: string) => {
const option = mapDesignOptions.find(item => item.design.id === designId);
if (!mapViewState || !option) return;
mapViewState.mapDesignType = option.design;
};04 · Defining a custom design
The preset types are the public constructors too. Give an id plus a style JSON URL (vector) or a tile URL template (raster) and your own style is treated exactly like a preset.
// MapLibreDesign is a data class: hand it a style JSON URL and you have your own design
val GsiPale =
MapLibreDesign(
id = "gsi-pale",
styleJsonURL = "https://example.com/styles/gsi-pale/style.json",
attributionRules =
listOf(
AttributionRule(attribution = "GSI Tiles"),
AttributionRule(attribution = "GEBCO", minZoom = 5, maxZoom = 8),
),
)private val mapLibreDesigns =
listOf(
MapDesignOption(label = "OsmBright", design = MapLibreDesign.OsmBright),
MapDesignOption(label = "GSI Pale", design = GsiPale),
)// Wraps the native mapType constant as it is and only adds the attribution
val NormalWithNotice =
GoogleMapDesign.Custom(
id = MAP_TYPE_NORMAL,
attributionRules = listOf(AttributionRule(attribution = "Our own data 2026")),
)Where the map kind is a native constant — Google Maps — use GoogleMapDesign.Custom, passing the constant as id and supplying your own attribution. MapTiler, Mapbox and HERE expose constructors of the same shape.
// public init(id:styleJsonURL:attributionRules:) is the way in for your own design
let gsiPale = MapLibreDesign(
id: "gsi-pale",
styleJsonURL: "https://example.com/styles/gsi-pale/style.json",
attributionRules: [
AttributionRule(attribution: "GSI Tiles"),
AttributionRule(attribution: "GEBCO", minZoom: 5, maxZoom: 8),
]
)private let mapLibreDesigns = [
MapDesignOption(label: "DemoTiles", design: MapLibreDesign.DemoTiles),
MapDesignOption(label: "GSI Pale", design: gsiPale),
]
// Cast to the provider's type when applying it
if let design = option.design as? MapLibreDesign {
mapLibreState.mapDesignType = design
}Being a struct, define it as a let and share it anywhere. The ViewModel option holds design: Any, so narrow it with as? when applying.
const GSI_STANDARD_DESIGN = new LeafletDesign({
id: 'gsi-standard',
tileUrl: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
tileOptions: { minZoom: 5, maxZoom: 18, tileSize: 256 },
attributionRules: GSI_STANDARD_ATTRIBUTION_RULES,
});
export const LEAFLET_DESIGNS: MapDesignOption[] = [
{ label: 'OpenStreetMap', design: LeafletDesign.OpenStreetMap },
{ label: 'GSI Standard', design: GSI_STANDARD_DESIGN },
{ label: 'None', design: LeafletDesign.None },
];// new MapLibreDesign(id, styleJsonURL, attributionRules?)
const GSI_PALE = new MapLibreDesign(
'gsi-pale',
'https://example.com/styles/gsi-pale/style.json',
[{ attribution: GSI_ATTRIBUTION }],
);
export const MAPLIBRE_DESIGNS: MapDesignOption[] = [
{ label: 'OsmBrightJa', design: MapLibreDesign.OsmBrightJa },
{ label: 'GSI Pale', design: GSI_PALE },
];Leaflet designs are raster tiles, MapLibre designs are style JSON. Design metadata lives in the same package as each provider heavy runtime SDK, so the sample imports it per provider on demand to keep the chunks separate.
05 · Attribution rules
When you bring your own tiles, carry the provider attribution conditions as rules. Only the rules that apply to the current zoom and viewport are shown.
06 · Sample apps
All three sample apps ship a design switcher. The UI is idiomatic per platform; only the assignment is shared.
val mapViewState = rememberMapLibreMapViewState(
mapDesign = MapLibreDesign.DemoTiles,
cameraPosition = MapCameraPosition(
position = GeoPoint.fromLatLong(21.382314, -157.933097),
zoom = 12.0,
),
)
val designs = listOf(
"Demo" to MapLibreDesign.DemoTiles,
"OSM Bright" to MapLibreDesign.OsmBrightJa,
"Toner" to MapLibreDesign.MapTilerTonerJa,
)
MapLibreMapView(state = mapViewState, modifier = Modifier.weight(1f))
Row {
designs.forEach { (label, design) ->
// Just an assignment. The map is not rebuilt, so the camera stays where it is
Button(onClick = { mapViewState.mapDesignType = design }) { Text(label) }
}
}