# Make it installable

> Go beyond the minimum bar: meet every installability criterion and design an install moment that earns the add-to-home-screen.

At the end of this guide the app passes Chrome's install criteria, shows its own install
button only on browsers that can honour it, offers iOS users the Share-sheet route instead
of a dead button, and knows at runtime whether it is running installed. Start from a site
that already meets the floor in [Getting started](/guides/getting-started/); this guide
builds the install experience on top of it.

## Clear Chrome's full criteria, including engagement

web.dev's install-criteria article lists what Chrome checks before firing
`beforeinstallprompt`: the app is not already installed, the user has clicked or tapped the
page at least once and spent at least 30 seconds on it (at any time, including a previous
visit), the page is served over HTTPS, and the manifest has `name` or `short_name`, 192 px
and 512 px `icons`, `start_url`, a `display` of `fullscreen`, `standalone`, `minimal-ui`,
or `window-controls-overlay`, and no `prefer_related_applications: true`. The engagement
rule is the one most often mistaken for a bug: on a fresh profile the event does not fire
during the first half minute. Set `scope` so that every route the app owns stays inside the
installed window; a URL outside `scope` opens with browser UI around it.

## Capture the prompt and show your own button

In Chromium browsers (Chrome, Edge, Samsung Internet) the browser fires
`beforeinstallprompt` once the criteria hold. Call `preventDefault()` to suppress the
browser's own banner on mobile, keep the event, and reveal a button. `prompt()` resolves
with `outcome` set to `"accepted"` or `"dismissed"` (MDN, `BeforeInstallPromptEvent.userChoice`),
and a deferred event can be prompted only once:

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

window.addEventListener('beforeinstallprompt', (event) => {
  event.preventDefault();
  deferredPrompt = event;
  installButton.hidden = false;
});

installButton.addEventListener('click', async () => {
  if (!deferredPrompt) return;
  const { outcome } = await deferredPrompt.prompt();
  deferredPrompt = null;
  installButton.hidden = true;
  if (outcome === 'dismissed') {
    localStorage.setItem('install-dismissed-at', String(Date.now()));
  }
});

window.addEventListener('appinstalled', () => {
  installButton.hidden = true;
  deferredPrompt = null;
});
```

The button starts hidden and stays hidden on Firefox and Safari, which do not fire the event
(MDN lists the event as not Baseline). The `appinstalled` listener also hides the button
when the user installs through the browser menu rather than your button. Show the button
after an action that signals intent, such as saving a second item; a prompt on first paint
asks for a commitment before the app has shown any value, and a dismissal records a
timestamp you can use to wait a week before asking again.

## Give iOS users the Share-sheet instructions

Safari on iOS and iPadOS has no `beforeinstallprompt`; the route is the Share button and
then **Add to Home Screen** (English UI), as WebKit's Web Push announcement describes. MDN
notes that from iOS 16.4 the same Share-menu entry exists in Chrome, Edge, Firefox, and
Orion on iOS. Detect the platform and the standalone state, then show instructions instead
of a button:

```js
const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent) && !window.MSStream;
const isStandalone =
  window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;

if (isIos && !isStandalone) {
  document.querySelector('#ios-install-hint').hidden = false; // "Tap Share, then Add to Home Screen"
}
```

`navigator.standalone` is the Safari-only property for a Home Screen web app; the
`display-mode` media query is the cross-browser check and is what web.dev's customize-install
article uses for its `getPWADisplayMode()` helper.

## Verify each path

Desktop Chrome shows an install icon at the right of the address bar on a page that meets
the criteria; the menu path is **More** > **Cast, save, and share** > **Install page as
app…** (English UI, Chrome help). On Android the path is **More** > **Install and create
shortcut** > **Install**. After installing, launch the app and confirm that
`matchMedia('(display-mode: standalone)').matches` is `true` in the console and that the
install button is hidden. Uninstall from the installed window via **More** > **Uninstall
[app name]** > **Remove** to retest the flow from a clean state.

:::observed
Chrome's help page "Use web apps" (English UI, read 2026-10-03) names the Android menu item
**Install and create shortcut**, not "Install app" or "Add to Home screen". A hint that tells
Android users to look for "Add to Home screen" sends them to a label Chrome no longer shows;
quote the current label or point at the install icon in the address bar instead.
:::

## See also

- [Getting started](/guides/getting-started/)
- [Install prompt](/reference/installation/install-prompt/)
- [iOS Add to Home Screen](/reference/installation/ios-add-to-home-screen/)
- [Install prompt compatibility](/compatibility/install-prompt/)
- [How to provide your own in-app install experience](https://web.dev/articles/customize-install) (web.dev)
- [Installation criteria](https://web.dev/articles/install-criteria) (web.dev)

← Back to the [Guides](/guides/) overview.