ai newsSa., 31. Okt.
EilmeldungClaude Code for VS Code 2.1.313 backdoor: how to check your machine and clean up

React PWA Install Prompt for Chrome, Edge and iPhone

TL;DR A React PWA install prompt needs two paths. In Chrome, Edge and Samsung Internet, catch beforeinstallprompt in a client component the root layout renders, call preventDefault, save the event and call its prompt() from your own Install button; on iPhone and iPad no browser fires that event, so

dev.toSamstag 08:00 · 31.10.13 Min. Lesezeit
Bild: dev.to

Posted on Oct 31 • Originally published at wingo-ui.com

A React PWA install prompt needs two paths. In Chrome, Edge and Samsung Internet, catch beforeinstallprompt in a client component the root layout renders, call preventDefault, save the event and call its prompt() from your own Install button; on iPhone and iPad no browser fires that event, so show the Share, then Add to Home Screen steps instead. Ask only after people get value, never inside the installed app, an in-app browser or a sign-up flow, and remember a Not now for a couple of weeks.

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.

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, MDN's installability guide and the Firefox 143 release notes:

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 ) as WONTFIX. On the Chromium side, the Chrome 157 release notes list a Web Install API ( navigator.install(), desktop first) and an <install> element, and Chromium Dash schedules Chrome 157 for stable on November 3, 2026. WebKit's standards position on that API is oppose, so it changes nothing on iPhone. Build on beforeinstallprompt today.

A manifest, HTTPS and some engagement. web.dev's install criteria 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 (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:

// app/manifest.ts import type { MetadataRoute } from " next "; export default function manifest (): MetadataRoute. Manifest { return { id: " / ", name: " Northwind CRM ", short_name: " Northwind ", start_url: " / ", display: " standalone ", background_color: " #ffffff ", theme_color: " #ffffff ", icons: [ { src: " /icon-192.png ", sizes: " 192x192 ", type: " image/png " }, { src: " /icon-512.png ", sizes: " 512x512 ", type: " image/png " }, { src: " /icon-maskable-512.png ", sizes: " 512x512 ", type: " image/png ", purpose: " maskable " }, ], }; } Enter fullscreen mode Exit fullscreen mode 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 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 ).

// hooks/use-before-install-prompt.ts import { useSyncExternalStore } from " react "; type BeforeInstallPromptEvent = Event & { prompt: () => Promise < void >; userChoice: Promise < { outcome: " accepted " | " dismissed "; platform: string } >; }; let deferred: BeforeInstallPromptEvent | null = null; let listening = false; const listeners = new Set < () => void > (); const notify = () => listeners. forEach (( listener ) => listener ()); function subscribe ( listener: () => void ) { // attached on the first subscribe, in the browser, and kept for the life of the page if (! listening ) { listening = true; window. addEventListener ( " beforeinstallprompt ", ( event ) => { event. preventDefault (); // no mini-infobar: the app decides when to ask deferred = event as BeforeInstallPromptEvent; notify (); }); window. addEventListener ( " appinstalled ", () => { deferred = null; notify (); }); } listeners. add ( listener ); return () => { listeners. delete ( listener ); }; } export function useBeforeInstallPrompt () { const event = useSyncExternalStore ( subscribe, () => deferred, () => null ); // call it from a click handler async function install () { const current = deferred; if (! current ) return " unavailable " as const; deferred = null; // prompt() works once per event notify (); await current. prompt (); const { outcome } = await current. userChoice; return outcome; } return { canInstall: event!== null, install }; } Enter fullscreen mode Exit fullscreen mode 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, after a dismissal the browser usually fires a fresh event right after userChoice resolves, which the store picks up.

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 tests /iPad|iPhone|iPod/ only, which misses current iPads, and its Add to Home Screen button has no handler.

// call it in an effect or a store snapshot, never during the server render export function getInstallContext () { const ua = navigator. userAgent; const ios = /iPhone|iPod|iPad/. test ( ua ) || ( /Macintosh/. test ( ua ) && navigator. maxTouchPoints > 1 ); // Instagram, Facebook, TikTok and Android WebViews have no install path const inApp = /FBAN|FBAV|Instagram|musical_ly|TikTok|; wv \) /i. test ( ua ); const standalone = window. matchMedia ( " (display-mode: standalone) " ). matches || ( navigator as Navigator & { standalone?: boolean }). standalone === true; return { ios, inApp, standalone }; } Enter fullscreen mode Exit fullscreen mode iOS 26 changed two things. WebKit's Safari 26 beta announcement 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 in September 2025, and iPhone Life's iOS 26.4 walkthrough 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.

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.

Live demo: Install Prompt. The original article embeds an interactive demo here (Wait a second for the banner, then tap Install: on an iPhone there is no browser dialog, so a sheet lists the three steps with the icons to look for. The × hides it for 14 days; Show again brings it back). Try it live on wingo-ui.com or open the Install Prompt docs. Install: npx wingo-ui@latest add install-prompt

The 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.

Live demo: Install Prompt. The original article embeds an interactive demo here (Tap Install: the demo plays Chrome's install dialog, so the button spins, then reads Installed with a check and the banner leaves. Show again replays it). Try it live on wingo-ui.com or open the Install Prompt docs. Install: npx wingo-ui@latest add install-prompt

Mount 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:

// components/app-install-prompt.tsx, rendered by app/layout.tsx after {children} " use client "; import { usePathname } from " next/navigation "; import { InstallPrompt, useInstallPrompt } from " @/components/ui/install-prompt "; // screens where a banner would interrupt a task const BUSY = /^ \/( welcome|signup|checkout )(\/ |$ ) /; export function AppInstallPrompt () { // starts the beforeinstallprompt listener even when the first page people open is a busy one useInstallPrompt (); const pathname = usePathname (); if ( BUSY. test ( pathname )) return null; return ( < InstallPrompt appName = "Northwind CRM" appIcon = "/icon-192.png" description = "Open it right from your home screen, even offline." delay = { 5000 } dismissDays = { 30 } labels = { { iosSteps: [ " Tap Share, or ⋯ and then Share ", " Choose Add to Home Screen (under View More if needed) ", " Tap Add " ], } } /> ); } Enter fullscreen mode Exit fullscreen mode 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.

Ask after people have gotten value, and stay quiet everywhere else. web.dev's install promotion patterns 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:

Ask for install after onboarding, and on iPhone ask for notifications inside the installed app. WebKit's iOS 16.4 announcement 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:

Live demo: Onboarding Flow. The original article embeds an interactive demo here (Tap Allow on each card: this demo answers as a browser that granted it. Continue stays enabled, so both permissions are optional). Try it live on wingo-ui.com or open the Onboarding Flow docs. Install: npx wingo-ui@latest add onboarding-flow

// app/welcome/page.tsx " use client "; import { useRouter } from " next/navigation "; import { OnboardingFlow, type OnboardingPermission } from " @/components/blocks/onboarding-flow "; import { useInstallPrompt } from " @/components/ui/install-prompt "; export default function WelcomePage () { const router = useRouter (); const { platform, isStandalone } = useInstallPrompt (); // web push on iPhone and iPad works in the Home Screen app, so a Safari tab skips that card const permissions: OnboardingPermission [] = platform === " ios " &&! isStandalone? [ " location " ]: [ " notifications ", " location " ]; return ( < OnboardingFlow persistKey = "onboarding" permissions = { permissions } onComplete = { async ( result ) => { await fetch ( " /api/onboarding ", { method: " POST ", body: JSON. stringify ( result ) }); // /welcome is on the BUSY list, so the install banner waits for the home route router. push ( " / " ); } } /> ); } Enter fullscreen mode Exit fullscreen mode The 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.

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.

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.

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.

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.

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.

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.

This article was originally published on the Wingo UI blog: React PWA Install Prompt for Chrome, Edge and iPhone. The original has the live, interactive demos and is kept up to date.

Quelle: dev.to (dev.to). Original öffnen