|
| 1 | +--- |
| 2 | +slug: install-button-for-websites |
| 3 | +title: "An install button for your website - and why the Play Store becomes optional" |
| 4 | +authors: [stesee] |
| 5 | +tags: [pwa, javascript, android, play store, web app manifest] |
| 6 | +--- |
| 7 | + |
| 8 | +A website can put its own INSTALL button into its UI. One click, and it sits on the home screen or in the Start menu, opens in its own window and works offline. None of the Play Store hassle is needed for that: no developer account, no registration fee, no revenue share, no closed test and no review queue. There is no package to build, sign or upload either - the deployed site is the app, and a new deployment is the update. This post shows the two pieces needed: a manifest and an event handler. |
| 9 | + |
| 10 | +<!-- truncate --> |
| 11 | + |
| 12 | +## Step 1: the manifest |
| 13 | + |
| 14 | +Chromium only offers installation when the page links a web app manifest. This is the one from our [Push Up Game](https://pushupgame.codeuctivity.cloud/): |
| 15 | + |
| 16 | +```json title="public/manifest.webmanifest" |
| 17 | +{ |
| 18 | + "id": "/", |
| 19 | + "name": "Push Up Game", |
| 20 | + "short_name": "Push Up Game", |
| 21 | + "start_url": "/", |
| 22 | + "scope": "/", |
| 23 | + "display": "standalone", |
| 24 | + "background_color": "#081820", |
| 25 | + "theme_color": "#081820", |
| 26 | + "icons": [ |
| 27 | + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, |
| 28 | + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, |
| 29 | + { "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } |
| 30 | + ] |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +```html title="index.html" |
| 35 | +<link rel="manifest" href="/manifest.webmanifest" /> |
| 36 | +<!-- Safari on iOS takes the home screen icon from here --> |
| 37 | +<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" /> |
| 38 | +``` |
| 39 | + |
| 40 | +[Chrome's install criteria](https://web.dev/articles/install-criteria): |
| 41 | + |
| 42 | +- served over HTTPS (`localhost` counts for development) |
| 43 | +- `name` or `short_name`, `start_url`, and `icons` with a 192px and a 512px entry |
| 44 | +- `display` set to `fullscreen`, `standalone`, `minimal-ui` or `window-controls-overlay` |
| 45 | +- `prefer_related_applications` absent or `false` |
| 46 | +- the user has clicked or tapped the page once and spent at least 30 seconds on it |
| 47 | + |
| 48 | +The last point matters for testing: the install event does not fire on page load of a first visit. |
| 49 | + |
| 50 | +A service worker is [no longer required](https://developer.chrome.com/blog/update-install-criteria) for installation from the browser menu (Chrome 108 on mobile, 112 on desktop). Register one anyway, otherwise the installed app shows the browser's offline error page without a network. |
| 51 | + |
| 52 | +Set `id` explicitly. Without it the app's identity is derived from `start_url`, and changing that URL later creates a second app instead of an update. |
| 53 | + |
| 54 | +## Step 2: the `beforeinstallprompt` event |
| 55 | + |
| 56 | +There is no `navigator.install()` to call from a click handler. When the criteria are met, Chromium fires `beforeinstallprompt` on `window`; the event object's `prompt()` method raises the install dialog. The page keeps the event and uses it later. |
| 57 | + |
| 58 | +```ts title="pwa.ts" |
| 59 | +interface BeforeInstallPromptEvent extends Event { |
| 60 | + prompt(): Promise<{ outcome: "accepted" | "dismissed" }>; |
| 61 | +} |
| 62 | + |
| 63 | +/** |
| 64 | + * Calls `onAvailable` with a function that raises the install dialog, |
| 65 | + * and with `null` once it has been used or the app is installed. |
| 66 | + */ |
| 67 | +export function installPrompt(onAvailable: (prompt: (() => void) | null) => void): void { |
| 68 | + window.addEventListener("beforeinstallprompt", (event) => { |
| 69 | + // Keep the browser's own mini-infobar from showing up. |
| 70 | + event.preventDefault(); |
| 71 | + const deferred = event as BeforeInstallPromptEvent; |
| 72 | + onAvailable(() => { |
| 73 | + onAvailable(null); |
| 74 | + void deferred.prompt(); |
| 75 | + }); |
| 76 | + }); |
| 77 | + window.addEventListener("appinstalled", () => onAvailable(null)); |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +```ts title="main.ts" |
| 82 | +const button = document.querySelector<HTMLButtonElement>("#install")!; |
| 83 | +button.hidden = true; |
| 84 | + |
| 85 | +installPrompt((prompt) => { |
| 86 | + button.hidden = prompt === null; |
| 87 | + button.onclick = prompt; |
| 88 | +}); |
| 89 | +``` |
| 90 | + |
| 91 | +What is easy to get wrong: |
| 92 | + |
| 93 | +- **The button starts hidden** and only appears once the browser has announced that installation is possible. |
| 94 | +- **`prompt()` works once per event**, whatever the user chose, so the handler withdraws the button before calling it. |
| 95 | +- **`prompt()` needs a user gesture.** Call it from the click handler. |
| 96 | +- **`appinstalled` covers the other path**: the user may install through the address bar icon or the browser menu instead. |
| 97 | + |
| 98 | +## Browser support |
| 99 | + |
| 100 | +`beforeinstallprompt` is a WICG draft that [only Chromium implements](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeinstallprompt_event). Installing works in most other browsers too, but through their own menu. |
| 101 | + |
| 102 | +| Platform | Browser | INSTALL button | How to install | |
| 103 | +| --- | --- | --- | --- | |
| 104 | +| Android | Chrome, Edge, Samsung Internet | Yes | Button, or browser menu → "Add to Home screen" / "Install app" | |
| 105 | +| Windows, macOS, Linux, ChromeOS | Chrome, Edge | Yes | Button, or the install icon in the address bar; own window and a Start menu / taskbar entry | |
| 106 | +| iPhone / iPad | Safari | No | Share → "Add to Home Screen" | |
| 107 | +| iPhone / iPad | Chrome, Edge, Firefox | No | Share menu → "Add to Home Screen" (iOS 16.4 and later) | |
| 108 | +| Desktop | Firefox | No | Not supported; the app only runs in a tab | |
| 109 | +| Mac | Safari | No | File → "Add to Dock" (macOS Sonoma and later) | |
| 110 | + |
| 111 | +## Summary |
| 112 | + |
| 113 | +A manifest and a 20-line event handler make a website installable with one click on Android and on desktop Chromium. Nothing but a deployment stands between `git push` and the user's home screen. |
| 114 | + |
| 115 | +The source of this blog, including this post, is on GitHub: [Codeuctivity/codeuctivity.github.io](https://github.com/Codeuctivity/codeuctivity.github.io). |
| 116 | + |
| 117 | +For questions or feedback, contact us at [Codeuctivity@gmail.com](mailto:Codeuctivity@gmail.com). |
0 commit comments