How do you show a PWA install banner on iOS?
My Recipe App shows an install banner on Android and iOS. Android is easy: the browser fires beforeinstallprompt and you catch it. iOS fires nothing, so you detect iOS Safari yourself and walk the user through Add to Home Screen by hand.
How do you get a website to pop up one of those Add to Home Screen banners on an iPhone? I went looking for the answer while building My Recipe App, a little Next.js app where I keep recipes, because I wanted people to be able to install it like a real app instead of bookmarking a tab.
The short answer is: on Android you can't lose, because the browser hands you an event and you just catch it. On iOS you can't win the same way, because Safari fires no event at all. So the iOS banner isn't really an install button. It's a set of instructions you show to the right person at the right moment. The interesting part is everything in between, and that's what this post is about.
First, why bother at all? A PWA, a progressive web app, is just a website that can be installed to the home screen and opened without the browser chrome around it. For a recipe app that matters. People cook with one floury hand and a phone propped against the kettle. An icon on the home screen that opens straight to their recipes is nicer than hunting for a Safari tab. So I wanted the install banner. I just didn't expect it to be two different problems wearing one coat.
Why is Android the easy one?
On Chrome for Android, the browser decides your site is installable and then fires an event called beforeinstallprompt. Think of it like a doorbell the browser rings when it's ready to let the user install. You don't get to ring it yourself, but you can answer it. The trick is that you catch that event, call preventDefault() so the browser doesn't show its own mini-bar, and stash the event on a variable. Later, when the user taps your nice custom button, you call .prompt() on the saved event and the real install dialog appears.
Here's the part of my hook that catches the doorbell. It runs once on the client, sets up a couple of listeners, and remembers the event for later.
export function useInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
const [isIOS, setIsIOS] = useState(false);
useEffect(() => {
// Detect iOS (iPhone / iPad / iPod)
setIsIOS(/iphone|ipad|ipod/i.test(navigator.userAgent));
// Detect already installed (standalone mode)
const standaloneQuery = window.matchMedia("(display-mode: standalone)");
const iosStandalone = (navigator as { standalone?: boolean }).standalone === true;
setIsInstalled(standaloneQuery.matches || iosStandalone);
// Listen for the install prompt (Android / Desktop Chrome/Edge)
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);
return { canPrompt: deferredPrompt !== null, isIOS, isInstalled, triggerInstall };
}That's basically the whole Android story. The browser tells me when it's installable, I save the event, and the button calls .prompt(). The user sees a native dialog, taps Install, and the app lands on the home screen. I'm mostly along for the ride, which is exactly how I like it.
Why does iOS need a workaround?
Because Safari never rings the doorbell. There is no beforeinstallprompt on iOS, and there's no API you can call to make the install dialog appear. The only way to install a PWA on an iPhone is for the user to do it manually: tap the Share button in Safari, scroll down, and tap Add to Home Screen. Your code can't do any of that for them.
I found this out the embarrassing way. I'd gated my banner on the Android event, then opened the site on my own iPhone, and the banner just wasn't there. Of course it wasn't. The event it was waiting for never fires on iOS, so a banner that waits for it can wait forever. The iOS user gets no signal that the app is installable at all.
So the workaround has two halves. First, figure out when to show the iOS banner, since no event will tell you. The answer is two checks: is this an iPhone or iPad, and is the app not already installed? I detect iOS straight off the user agent string (unfashionable, but for the narrow question of whether this browser supports the install event, it's the cheapest reliable signal), and I detect already-installed by reading navigator.standalone, the iOS-only flag that's true only when the page is running as the home-screen app. Both of those checks are in the hook above, right at the top of the effect.
Second, since you can't install for them, you show them how. On iOS my button says How to instead of Install, and tapping it opens a little bottom sheet with three numbered steps. My first attempt was a one-line tooltip that said "tap Share then Add to Home Screen," and it was useless. On a phone, nobody knows which icon Share is, or that they have to scroll. The instruction has three steps and a prerequisite, so it needs to look like steps, not a sentence.
Here's the sheet. The detail that makes it work is step two: it renders the actual Lucide Share icon inline, so the user can pattern-match it against the same icon sitting in their Safari toolbar.
export function IosInstallSheet({ open, onOpenChange }: Props) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="bottom" className="rounded-t-2xl px-6 pb-10">
<SheetHeader className="mb-6 text-left">
<SheetTitle>Add to Home Screen</SheetTitle>
</SheetHeader>
<ol className="space-y-5 px-1">
<li className="flex items-start gap-4">
<span className="… rounded-full bg-zinc-900 text-white">1</span>
<p className="text-sm text-zinc-700">
Make sure you're using <span className="font-semibold">Safari</span>, the
install option only works in Safari on iOS.
</p>
</li>
<li className="flex items-start gap-4">
<span className="… rounded-full bg-zinc-900 text-white">2</span>
<p className="text-sm text-zinc-700">
Tap the{" "}
<span className="inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs">
<Share size={12} />
Share
</span>{" "}
button at the bottom of the screen.
</p>
</li>
<li className="flex items-start gap-4">
<span className="… rounded-full bg-zinc-900 text-white">3</span>
<p className="text-sm text-zinc-700">
Scroll down in the menu and tap{" "}
<span className="font-semibold">"Add to Home Screen"</span>, then tap{" "}
<span className="font-semibold">Add</span>.
</p>
</li>
</ol>
</SheetContent>
</Sheet>
);
}Step one is the prerequisite: you have to be in Safari. The install option just isn't there in Chrome or Firefox on iOS, which I learned by tapping Share in Chrome and scrolling the menu twice waiting for an entry that was never coming. Step two is the action, with the icon shown inline. Step three names the menu item in quotes so the user knows the exact words to look for. That's the whole iOS workaround: detect the right user, then teach them the three taps you can't do for them.
Things that surprised me
A few things I had to learn the hard way while wiring this up:
- iOS gives you no after-install signal in Safari. Android fires an appinstalled event and navigator.standalone only reads true inside the installed app, so a regular Safari tab never knows the user already installed. I gave up on detecting it and just hid the banner for two days after a dismissal using localStorage.
- The Add to Home Screen option only exists in Safari. Chrome and Firefox on iOS both hide it, which is why step one of my sheet is just "make sure you're in Safari."
- On iPad the Share button is top-right, not bottom. I almost branched the copy for iPad, then realized the icon is identical in both places. Telling people to find the icon works better than telling them where it sits.
- Start the banner hidden, then reveal it. If it starts visible, it flashes for one frame before the cooldown check hides it again, and on a phone that flash looks like a popup snapping shut in your face.
So is it worth it?
If your app is something people open more than once on their phone, yes. A recipe app lives or dies on being one tap away mid-cook, so the banner earns its keep. If it's a thing people visit once and leave, I'd skip it: the iOS half is real work for a payoff almost nobody will take.
If you do build it, here's what I'd tell you going in. Treat Android and iOS as two separate features that happen to share a bar at the bottom of the screen. Catch beforeinstallprompt for Android and let the browser do the work. For iOS, detect the iPhone and the not-yet-installed state yourself, then show clear numbered steps with the Share icon drawn right there in the sheet. You can't make Safari install your app. The best you can do is make the three taps impossible to get wrong, and honestly, that turned out to be enough.


