Showing a native map in a Capacitor app has practically meant one thing: Google Maps, plus the billing account it requires before the first tile loads. We just released the Capacitor MapLibre plugin, the first native MapLibre integration for Capacitor — free, open stack, and no vendor account required by the plugin.
It renders maps with the native MapLibre SDKs on Android and iOS and with MapLibre GL JS on the Web, so one TypeScript API covers all three platforms.
How the Native Rendering Works
On Android and iOS, the map is a native view rendered behind the web view, positioned by an empty element in your DOM. The contract is small: the map element stays empty, and it and every ancestor covering the map region use background: transparent — otherwise the web view paints over the map.
The upside of this architecture: any DOM element that isn't an ancestor of the map element renders above the map. Floating action buttons, bottom sheets, and dialogs just work. The plugin also keeps the native view in sync with the element's position and size automatically, including while the page scrolls.
Create a Map
import { MapLibre } from '@capawesome/capacitor-maplibre';
const createMap = async () => {
await MapLibre.createMap({
center: { latitude: 48.137154, longitude: 11.576124 },
elementId: 'map',
mapId: 'my-map',
styleUrl: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
zoom: 12,
});
};
Every method takes the mapId, so multiple maps can run at the same time.
Markers with Animated Updates
Markers take custom icons with configurable anchor, size, and rotation, and can be moved with a smooth animation — the building block for live vehicle tracking:
import { MapLibre, MarkerIconAnchor } from '@capawesome/capacitor-maplibre';
const addAndMoveMarker = async () => {
await MapLibre.addMarker({
mapId: 'my-map',
marker: {
coordinates: { latitude: 48.137154, longitude: 11.576124 },
iconAnchor: MarkerIconAnchor.Center,
iconSize: { height: 32, width: 32 },
iconUrl: 'https://example.com/marker.png',
id: 'my-marker',
},
});
await MapLibre.updateMarkerById({
animate: true,
animationDuration: 1000,
coordinates: { latitude: 48.370545, longitude: 10.89779 },
mapId: 'my-map',
markerId: 'my-marker',
rotation: 90,
});
};
GeoJSON Layers
For routes, areas, and anything your backend already stores as GeoJSON, add a source and style it with line, fill, or circle layers:
import { LayerType, MapLibre } from '@capawesome/capacitor-maplibre';
const addGeoJson = async () => {
await MapLibre.addGeoJsonSource({
mapId: 'my-map',
sourceId: 'my-source',
url: 'https://example.com/routes.geojson',
});
await MapLibre.addLayer({
layerId: 'my-layer',
mapId: 'my-map',
paint: { lineColor: '#3887be', lineWidth: 4 },
sourceId: 'my-source',
type: LayerType.Line,
});
};
User Location
Request the location permission, then display and follow the user:
import { MapLibre, UserTrackingMode } from '@capawesome/capacitor-maplibre';
const enableUserLocation = async () => {
let status = await MapLibre.checkPermissions();
if (status.location === 'prompt') {
status = await MapLibre.requestPermissions();
}
if (status.location !== 'granted') {
return;
}
await MapLibre.enableUserLocation({
mapId: 'my-map',
trackingMode: UserTrackingMode.Follow,
});
};
Map Styles and API Keys
The plugin renders any style following the MapLibre Style Spec. "No API key required by the plugin" is not the same claim as "free tiles": the style decides where tiles come from. Free providers such as OpenFreeMap and CARTO basemaps exist, while commercial providers such as MapTiler use their own keys. Follow your provider's attribution requirements, and don't ship the default demo style — it's for testing only.
Availability
The plugin is free, requires Capacitor 8 or later, and one known limitation applies: offline tile management is not part of this version. The full announcement is on our blog: Announcing the Capacitor MapLibre Plugin, and the plugin documentation covers the complete API.
Questions or feedback? Drop a comment — happy to answer.
Top comments (0)