React PWA Install Prompt for Chrome, Edge and iPhone
A React PWA install prompt has to cover two kinds of browsers. Chrome, Edge and Samsung Internet fire beforeinstallprompt, so your own button can open the browser's install dialog. No browser on iPhone or iPad fires it, so the only way in is Share, then Add to Home Screen, and your app has to show people where that is. The usual bugs are a listener in a component that mounts too late and a banner that asks on the first page view. This tutorial builds both paths in Next.js, then covers when not to ask at all.
Which browsers let a page open the install dialog?
Only Chromium browsers. Everywhere else people install from a browser menu, if at all, and the page can only explain where. As of October 2026, per caniuse (opens in a new tab), MDN's installability guide (opens in a new tab) and the Firefox 143 release notes (opens in a new tab):
On iPhone, no beforeinstallprompt iOS polyfill can help, because no iOS browser has an install dialog a page can open, and WebKit closed the 2019 request for BeforeInstallPromptEvent (bug 193959 (opens in a new tab)) as WONTFIX. On the Chromium side, the Chrome 157 release notes (opens in a new tab) list a Web Install API (opens in a new tab) (navigator.install(), desktop first) and an <install> element, and Chromium Dash (opens in a new tab) schedules Chrome 157 for stable on November 3, 2026. WebKit's standards position (opens in a new tab) on that API is oppose, so it changes nothing on iPhone. Build on beforeinstallprompt today.
What does Chrome need before it fires beforeinstallprompt?
A manifest, HTTPS and some engagement. web.dev's install criteria (opens in a new tab) require an app that is not installed yet, served over HTTPS, with a manifest that has name or short_name, a 192px and a 512px icon, start_url, and a display of standalone, fullscreen, minimal-ui or window-controls-overlay. The person must also have clicked or tapped the page once and spent 30 seconds on it, at any time. That is why your PWA install button can stay hidden on a first visit even with a perfect manifest. In Chrome DevTools, Application, then Manifest lists the installability errors.
A service worker is optional for installability, per MDN. Chrome's post Revisiting Chrome's installability criteria (opens in a new tab) (last updated December 2023) dropped the fetch() handler requirement for the menu install (Chrome 108 on mobile, 112 on desktop) but said the automatic install prompt still depended on one. If you depend on the event, register a service worker with a fetch handler anyway; offline support needs one too.
In the App Router the manifest is a file convention:
How do you capture beforeinstallprompt in React?
In a module-level store that starts listening when the root layout mounts, not in a component's state. The browser fires the event as soon as the page qualifies, often right after load, and an event that fired before your listener existed is gone, so a listener in a settings page that mounts later never hears it. Call preventDefault() to keep Chrome's own mini-infobar away, store the event, and call prompt() from a click: the web.dev codelab (opens in a new tab) calls it in the button's click handler because it must come from a user gesture, and resets the saved event because prompt() works once. The event type is not in lib.dom.d.ts, because it is Chromium-only and non-standard (MDN (opens in a new tab)).
Call the hook in a client component that the root layout renders, and show your button only while canInstall is true. useSyncExternalStore returns null on the server and during hydration, so the button never causes a hydration mismatch. userChoice reports what happened in the dialog your button opened; appinstalled fires for every install, including one from the address bar. Per web.dev (opens in a new tab), after a dismissal the browser usually fires a fresh event right after userChoice resolves, which the store picks up.
How do you guide Add to Home Screen on iOS?
Detect iPhone and iPad, skip it inside the installed app and in in-app browsers, and show the steps with the icons people will look for. iPadOS reports a Mac user agent, so check touch points too. The PWA guide in the Next.js docs (opens in a new tab) tests /iPad|iPhone|iPod/ only, which misses current iPads, and its Add to Home Screen button has no handler.
iOS 26 changed two things. WebKit's Safari 26 beta announcement (opens in a new tab) says every website added to the Home Screen now opens as a web app by default, with an Open as Web App switch people can turn off. And in Safari's default Compact tab layout, Share sits behind the More (⋯) button, so older Add to Home Screen iOS instructions miss a tap. App builders flagged it in a Glide community thread (opens in a new tab) in September 2025, and iPhone Life's iOS 26.4 walkthrough (opens in a new tab) adds a View More tap before Add to Home Screen. The Bottom and Top layouts keep Share on the toolbar, so write steps that work in all three.
How does the Wingo UI Install Prompt handle both?
It is one component for both paths. Install Prompt keeps the event in a page-wide store, like the hook above, and adds what the hook skips: platform detection for Chromium, iPhone, iPad and Safari on a Mac, the standalone and in-app checks, a remembered Not now, and the UI. It renders nothing where there is no install path.
$ npx wingo-ui@latest add install-promptThe demo forces platform="ios"; in your app the default auto detects it. The banner shows delay milliseconds (3000 by default) after an install becomes possible, never takes focus, and sits above a Bottom Tab Bar and the home indicator through --bottom-bar-offset and the safe area insets. On iPhone, Install opens a Drawer with numbered steps and an arrow toward Safari's share button; from 768px up it becomes a centered dialog. On a Mac, Safari gets the File, then Add to Dock steps inline.
$ npx wingo-ui@latest add install-promptMount it once, high in the tree. This wrapper also keeps it off the screens where it would interrupt a task, and rewrites the iPhone steps for iOS 26:
For a settings page, variant="card" places the same prompt inline. For a menu item, useInstallPrompt() returns platform, canInstall, isStandalone and promptInstall(), which resolves accepted, dismissed or unavailable.
When should you not ask people to install?
Ask after people have gotten value, and stay quiet everywhere else. web.dev's install promotion patterns (opens in a new tab) say disruptive promotions hurt usability and engagement, that a sign-up form should not be interrupted, and that a turned-down promotion should stay hidden until something changes, such as a sign-in or a purchase. Skip it:
- On arrival. Chrome already waits for a tap and 30 seconds; iPhone has no such gate, so your
delayis the only one. - Inside a task. Sign-up, checkout and onboarding are on the
BUSYlist above. For a multi-step flow, wait until the last step is done. - Inside the installed app. Check
display-mode: standaloneandnavigator.standalone. The component also treatsminimal-uiandwindow-controls-overlayas installed. - In an in-app browser. A link opened from Instagram or TikTok has no install path, and the Install Prompt renders nothing there. If much of your traffic arrives that way, the MIT-licensed pwa-install (opens in a new tab) web component added in-app browser detection with guided steps, such as opening the link in Safari, in 0.7.0 (opens in a new tab) (September 2026). Ours does not do that.
- On top of another banner. One floating message at a time. Fixed install banners queue behind each other, but a cookie banner is yours to sequence.
- Again right after Not now. The × stores a timestamp in localStorage under
storageKeyand hides the prompt fordismissDays(14 by default;0means this session only).
How do you pair the install prompt with onboarding?
Ask for install after onboarding, and on iPhone ask for notifications inside the installed app. WebKit's iOS 16.4 announcement (opens in a new tab) added Web Push for Home Screen web apps, with the permission request tied to a direct user interaction. The Onboarding Flow asks for notifications and location on its own step, so drop the notification card in a Safari tab:
$ npx wingo-ui@latest add onboarding-flowThe block's slides, interests, sample user and labels default to its Acme Market demo content, so pass slides, interests, user and labels for your own app before you ship it. Once people finish, the banner shows after its delay on the home route: they have just set the app up, and no form is competing for the screen. For the rest of the mobile layer, read mobile-first React components, add pull to refresh to the PWA, or browse Mobile UI in React.
What should you install next?
Add the component, then the manifest and the root layout wrapper above. npx wingo-ui@latest add install-prompt copies it with the Button, Drawer and hooks it imports, and installs the npm packages they use (motion, lucide-react, vaul and three Radix primitives). Install Prompt and Onboarding Flow are part of Wingo UI Pro, so sign in first with npx wingo-ui@latest login; the Button and Drawer underneath are free.
FAQ
Does beforeinstallprompt work on iOS?
No. No browser on iPhone or iPad fires it, and WebKit closed the 2019 request to add BeforeInstallPromptEvent (bug 193959) as WONTFIX. People install from the Share menu, in Safari or, since iOS 16.4, in other browsers that added the option such as Chrome and Edge, so the best a page can do is show those steps.
Why is beforeinstallprompt not firing?
Chrome fires it only for an app that is not installed yet, served over HTTPS, with a manifest that has a name, 192px and 512px icons, a start_url and a standalone-like display, and only after the person has tapped the page once and spent 30 seconds on it at some point. It often fires right after load, so a listener that mounts later on another route misses it; Application, then Manifest in Chrome DevTools lists what is missing.
How do I know if my PWA is already installed?
Inside the installed app, matchMedia('(display-mode: standalone)') matches, and on iOS navigator.standalone is true as well. In Chromium, the appinstalled event fires on the page at the moment of the install, whichever button started it.
Can I show the install prompt again after people dismiss it?
Each beforeinstallprompt event can call prompt() once; after a dismissal you wait for the browser to fire a new event, which web.dev says usually comes right after userChoice resolves. Your own banner should also remember Not now for a while; the Wingo UI Install Prompt hides it for 14 days by default.
Is the Wingo UI Install Prompt free?
No. The Install Prompt and the Onboarding Flow block are Pro items, while the Button and the Drawer the prompt builds on are free. With a Pro account signed in through npx wingo-ui@latest login, npx wingo-ui@latest add install-prompt copies it and everything it imports into your project.
- Mobile UI
- PWA
- Install prompt
- React
- Next.js