# PWAs on Chrome and Android (WebAPK, TWA)

> How PWAs install on Chrome for Android: the beforeinstallprompt flow, the WebAPK minted on GMS devices, manifest update timing, and Trusted Web Activity.

import CompatTable from '@components/CompatTable.astro';

On an Android device with Google Mobile Services (GMS), installing a PWA from Chrome
produces a WebAPK: a small APK that Chrome requests from a Google server and installs, so
the app gets a launcher entry, a row in Android's app settings, and intent filters for its
scope. Without GMS, Chrome adds a browser-badged home screen shortcut instead, and a
Google Play listing needs the same web app wrapped in a Trusted Web Activity (TWA).

## How it works

The form of install depends on the browser, not on the site. On Android, Chrome on GMS
devices and Samsung Internet on Samsung devices are the two browsers that install PWAs as
WebAPKs; Firefox, Edge, Opera, and Chrome on devices without GMS add a shortcut that opens
the site in the browser
([Making PWAs installable](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable),
MDN). Chrome for Android installs on every Android release it runs on, back to Jelly Bean.

<CompatTable feature="webapk" />

Trusted Web Activity is a separate mechanism with its own dataset:

<CompatTable feature="twa" />

### What Chrome needs before it offers an install

Chromium browsers, including Chrome, Samsung Internet, and Edge, promote a site for
installation only when its manifest has `name` or `short_name`, `icons` with a 192px and a
512px entry, `start_url`, `display` or `display_override`, and
`prefer_related_applications` either `false` or absent. The page must be served over
`https`, or from `localhost` or `127.0.0.1` during development; `file://` does not count,
even though it is a secure context. A service worker is not required for installability.
Chrome for Android, Chrome for desktop, Edge for desktop, and Safari for desktop also let
the user install a site that has no manifest at all; the manifest's job is to make the
browser promote the install and to shape the result.

The user-facing path in Chrome for Android (English UI) is the three-dot **More** menu,
then **Install and create shortcut**, then **Install**
([Chrome Help](https://support.google.com/chrome/answer/9658361?co=GENIE.Platform%3DAndroid)).
A page can also open the same dialog itself from a `beforeinstallprompt` handler; the
code is under "Examples".

### What the WebAPK gives you

Chrome builds the WebAPK from the manifest and other metadata
([WebAPKs on Android](https://web.dev/articles/webapks), web.dev). Once installed:

- The app opens in the Chrome the user installed it from, not in a WebView.
- It registers intent filters for every URL inside `scope`. With `"scope": "/app/"`,
  tapping a link to `/app/read/book` opens the app; `/help/` opens a browser tab.
- Permissions are not granted at install. Android grants native apps notification
  permission on install, but a WebAPK must request it at runtime, through Chrome's own
  prompts and settings.
- Storage is the browser profile's storage. Cookies, IndexedDB, Cache Storage, and the
  service worker registration are shared with Chrome; clearing Chrome's site data clears
  the installed app too.
- WebAPKs generated by Chrome 71 or later show a larger icon on the splash screen when the
  manifest ships a 512px icon.
- The generated APK is not accepted by Google Play. A Play listing is what a TWA is for.

### When the manifest changes

Chrome for Android schedules a manifest fetch on launch if the last check is more than 24
hours old, and may stretch the interval to 30 days after a failed fetch
([How Chrome handles updates to the web app manifest](https://web.dev/articles/manifest-updates),
web.dev). A change to `name`, `short_name`, `icons`, `background_color`, `display`,
`orientation`, `scope`, `shortcuts`, `start_url`, `theme_color`, or `web_share_target`
queues a new WebAPK, which Chrome requests once every app window is closed and the device
is plugged in and on Wi-Fi. Renaming or moving the manifest file can stop updates
altogether, and a manifest that carries per-user values regenerates the APK for no
benefit.

### Trusted Web Activity for Google Play

A TWA opens your origin from an Android app you publish, through a protocol built on
Custom Tabs, available in Chrome 72 and later
([Trusted Web Activities](https://developer.chrome.com/docs/android/trusted-web-activity/overview),
developer.chrome.com). The app and the site must be linked with Digital Asset Links; when
they are, the content renders fullscreen in the user's browser, with no browser UI. The
host app has no direct access to cookies or `localStorage`, so state passes through URLs
(query parameters and intent URIs). On a Chrome older than 72 the same app falls back to a
Custom Tab with a plain toolbar. Bubblewrap is the Node.js CLI that generates the Android
project; the packaging steps are on the
[Trusted Web Activity (TWA): PWAs in the Play Store](/reference/installation/twa/) entry.

## Examples

Both examples run unchanged in a browser tab, in a WebAPK, and in a desktop app window;
the feature checks decide what the user sees.

### Showing your own install button

`beforeinstallprompt` fires on `window` once Chrome has decided the page is installable,
usually during page load, with no guaranteed timing. Keep the button hidden until the
event arrives, because the event is Chromium-only:

<CompatTable feature="install-prompt" />

```html
<button id="install" hidden>Install</button>
```

```js
let installPrompt = null;
const installButton = document.querySelector('#install');

window.addEventListener('beforeinstallprompt', (event) => {
  event.preventDefault();          // suppress Chrome's own install UI
  installPrompt = event;           // keep the event for the click handler
  installButton.removeAttribute('hidden');
});

installButton.addEventListener('click', async () => {
  if (!installPrompt) return;
  const consumedPrompt = installPrompt;
  installButton.disabled = true;   // no second click while the dialog is open
  try {
    const result = await consumedPrompt.prompt();
    console.log(`Install prompt was: ${result.outcome}`); // "accepted" or "dismissed"
  } finally {
    installPrompt = null;          // the instance is consumed on every exit path
    installButton.disabled = false;
    installButton.setAttribute('hidden', '');
  }
});
```

`prompt()` has to run inside a user-activation handler and may be called once per
`BeforeInstallPromptEvent` instance
([BeforeInstallPromptEvent: prompt() method](https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent/prompt),
MDN), which is why the handler retires the event in `finally` rather than only after a
successful prompt. On iOS the event does not exist, so the button stays hidden there and
the user installs from the Share menu.

### Reading the applied display mode

`display-mode` reports the mode the browser applied, which can differ from the manifest's
`display` when the browser does not support the requested value
([`display-mode`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/display-mode),
MDN). It describes presentation, not install state, so use it to adjust the UI, not as an
installation API:

```js
function applyDisplayMode(installButton) {
  if (!('matchMedia' in window)) {
    // matchMedia is unavailable: leave the button hidden and rely on the browser's menu.
    installButton.hidden = true;
    return;
  }
  if (window.matchMedia('(display-mode: standalone)').matches) {
    installButton.hidden = true; // already running as an app window
  }
}
```

A `standalone` match means the page is not in a normal tab; it does not prove a WebAPK
exists, because the same media query matches a desktop app window and a Home Screen web
app on iOS.

## Observed behaviour

The WebAPK update cycle is visible on-device, which is the quickest way to tell a stale
manifest from a cache problem.

:::observed
Chrome for Android lists the installed WebAPKs at `about://webapks` (English UI). Each
entry has an **Update** button that forces a manifest check; after a manifest change the
update status shown there reads "Pending" and switches to "Successful", typically within
a few minutes, as documented in web.dev's manifest-updates article (updated 2024-09-19).
A WebAPK that still shows the old icon after that is a caching problem, not an update
problem.
:::

Uninstalling goes through Android, not Chrome: **Settings** > **Apps** > **See all apps**,
tap the web app, then **Uninstall** (English UI, Chrome Help). When a developer changes
the app's `name`, Chrome shows the new name to the user with **OK** to accept or
**Uninstall app** to remove it; a rename that imitates another app is treated by Chrome
Help as a sign the developer may be malicious.

## See also

- [WebAPKs on Android](https://web.dev/articles/webapks) (web.dev)
- [How Chrome handles updates to the web app manifest](https://web.dev/articles/manifest-updates) (web.dev)
- [Trusted Web Activities](https://developer.chrome.com/docs/android/trusted-web-activity/overview) (developer.chrome.com)
- [Installability criteria: what makes a PWA installable](/reference/installation/installability-criteria/)
- [WebAPK: how Chrome installs PWAs on Android](/reference/installation/webapk/)
- [Trusted Web Activity (TWA): PWAs in the Play Store](/reference/installation/twa/)
- [beforeinstallprompt and custom install prompts](/reference/installation/install-prompt/)
- [PWAs on Samsung Internet](/reference/platforms/samsung-internet/)