---
title: "How to create a mobile app (PWA) with MapTiler SDK JS | JavaScript maps SDK"
description: "Create mobile PWA using MapTiler SDK JS."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# How to create a mobile app (PWA) with MapTiler SDK JS

This example shows how to use MapTiler SDK to create a native mobile app that can be pushed to the Apple App Store and Google Play.

With this project as a starter (or any Capacitor app) and with a single codebase, you can create:

-   A web app hosted online, which people visit with a regular web browser.
-   A PWA that people can turn into a semi-app (under certain conditions: Android, EU-iPhone, etc.).
-   A Capacitor-powered native web app wrapped to be distributed on Apple AppStore and Google Play.

Note

PWAs or [Progressive Web Apps](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) are web apps hosted online, like any website, but they are shipped with a few extra settings in a manifest file. We are not going to cover this here, but it’s worth mentioning that your web app does **not** need to comply to the PWA set of rules to be wrapped inside a native mobile app with Capacitor. Those are two very different things, both with their pros and cons.

## Get started

For this project, we started from the [ViteJS Vanilla TypeScript sample project](https://vite.dev/guide/#trying-vite-online) and then followed the [Ionic Capacitor instructions](https://capacitorjs.com/docs/getting-started#add-capacitor-to-your-web-app) to give our project mobile superpowers.

To make things easier for you, we have created the [maptiler-mini-mobile-app](https://github.com/maptiler/maptiler-mini-mobile-app) repository where we have already configured all the dependencies, configuration files, styles, etc. Here’s how to set it up:

1.  Clone the [maptiler-mini-mobile-app](https://github.com/maptiler/maptiler-mini-mobile-app) repo.
2.  To install project dependencies, run `npx npm install`.
3.  Rename `.env.sample` to `.env` and replace the value of `YOUR_MAPTILER_CLOUD_API_KEY` with your actual [MapTiler API key](https://cloud.maptiler.com/account/keys/).
4.  Modify the file `capacitor.config.ts`: Update the app name, logo, bundleID, etc. For more info, refer to the [Capacitor configuration](https://capacitorjs.com/docs/config) docs.
5.  In terminal, run `npx npm run build` && `npx cap sync`.

### Modify the source

The project itself reuses the structure of the ViteJS boilerplate, meaning the project source is located in the `src/` directory and can be ran in a regular web browser:

-   In dev/watch mode: `npm run dev`
-   To build a production bundle: `npm run build`

After building the project for production, you can run it locally with `npm run preview`.

### Update the mobile app

Both mobile projects (XCode workspace and Android Studio Workspace) need to be updated after the web project has been built for production. To do so, run: `npm run sync`

Back in XCode or Android Studio, you may see a popup asking if you want to refresh the project based on the updates. Select “yes” (or “Read from disk” in XCode).

Then build the projects in XCode or Android Studio again and you should see your latest changes.

#### Customize the iOS project

First, run your project directly on iOS with the command `npx cap run ios` or open the iOS project with `npm run open-xcode`. You can then tune a few things.

Select the target iOS version:

![Select the iOS target version](./img/pwa-iOS-target-version.webp)

If you plan to distribute your app, set up **signing** by associating it to a **Team**:

![iOS app signing & capabilities](./img/pwa-signing-app.webp)

To change the icon of your app, go to menu App > App > Assets and drag and drop a non-transparent image in the icon frame:

![Change the iOS app icon](./img/pwa-change-app-icon.webp)

## What’s in this project?

### Mobile-friendly geolocation

The geolocation control originally available in MapTiler SDK is web-specific, and even though it will work when encapsulated into a mobile app, using it will trigger two user-agreement panel: one at the web-view level, and the next at system level. A more elegant way is to use `@capacitor/geolocation`, an official plugin that uses geolocation directly from the system. We had to modify the settings of the native projects (both Android and iOS) to grant the app the permission to use geolocation. You can read more about this on [the plugins’s page](https://capacitorjs.com/docs/apis/geolocation).

To make it simpler to integrate, we have created a SDK-friendly control that you can find in `src/universalgeolocatecontrol.ts`. Note that **this also works in a regular web page** as the plugin provides a complete fallback.

### Safe insets

The safe insets are the margins that need to be put so that the content you display on screen does not step on the system display, such as the status bar on top or the bottom menu on iOS. They are defined as CSS environment variables that you can directly leverage in your styling.

For instance, MapTiler SDK and MapLibre stylesheet define some classes that apply to the controls called `.maplibregl-ctrl-yyy-xxx`. Our mobile app can define more properties to these classes to prevent the visual elements to clash with the system display. You can see the classes with the insets in the file `src/style.css`.

Example of a class using safe insets for controls positioned in the top-right corner of the map:

```css
.maplibregl-ctrl-top-right {
  top: env(safe-area-inset-top);
}
```

The app’s viewport must also have the following attributes in the `index.html`:

```html
<meta
  name="viewport"
  content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
```

Here is the result:

| ❌ **Without safe insets** | ✅ **With safe insets** |
| --- | --- |
| ![Map applicaction controls without safe insets](./img/map-with-no-insets.webp) | ![Map applicaction controls with safe insets](./img/map-with-safe-insets.webp) |

Note

**Note**: The _safe insets_ values will be `0` in a web browser, so it will not impact your layout to include them in all cases.

## Related examples

### [Learn the basics](https://docs.maptiler.com/sdk-js/examples/how-to-use/)

Tutorials

This tutorial shows how to create a map and display it on a web page using MapTiler.

### [Start with AR maps](https://docs.maptiler.com/sdk-js/examples/ar-control/)

Examples

Add an AR control button to map 3D viewports.

### [Switch from MapLibre](https://docs.maptiler.com/sdk-js/examples/switch-from-maplibre/)

Tutorials

Steps to switch from MapLibre GL JS to MapTiler SDK.

### [Maps SDK TypeScript](https://docs.maptiler.com/sdk-js/examples/ts-get-started/)

Tutorials

Create maps using TypeScript for type safety and completion.
