# Install Prompt

> A React PWA install prompt: the browser's install dialog in Chrome and Edge, and an Add to Home Screen guide on iPhone and Mac.

- Type: React component (Mobile)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add install-prompt`
- Docs: https://wingo-ui.com/components/install-prompt

## Interaction

A few seconds after the page can be installed, a slim card with the app's icon, name and an Install button slides up from the bottom (or drops from the top) and stays out of the way: it never takes focus and the page keeps working under it. On Android and desktop Chrome or Edge, Install opens the browser's own install dialog; the button shows a spinner while it is open, and once the app is installed the card says Installed with a check for two seconds and leaves. On an iPhone or iPad, where browsers have no such dialog, Install opens a bottom sheet with three numbered steps (tap Share, choose Add to Home Screen, tap Add), each with the icon you will look for, and on an iPhone an arrow at the bottom of the sheet bobs twice toward Safari's share button. On a Mac with Safari the card shows the Add to Dock steps inline. The × (named Not now) hides it for dismissDays and remembers that in localStorage; closing the sheet by dragging it down, tapping outside or pressing Escape counts the same. On a desktop Chrome window the banner is a 380px card in the bottom end corner. With the keyboard, Tab reaches Install and ×, Enter or Space presses them, and in the sheet (a centered dialog from 768px up) Tab stays inside and Escape closes it, returning focus to the button that opened it. Installed or already running as an app, it renders nothing. The outcome is announced to screen readers. Under reduced motion it fades instead of sliding.

## Installation

```bash
npx wingo-ui@latest add install-prompt
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/install-prompt` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/install-prompt.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))

## Usage

```tsx
// app/layout.tsx: mount it once in the root layout, the browser offers the install only once and early
import type { ReactNode } from "react"
import { InstallPrompt } from "@/components/ui/install-prompt"

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <InstallPrompt
          appName="Northwind CRM"
          appIcon="/icon-192.png"
          description="Open it right from your home screen, even offline."
          labels={{
            notNow: "Maybe later",
            iosSteps: ["Tap Share in the bottom bar", "Choose Add to Home Screen", "Tap Add"],
          }}
        />
      </body>
    </html>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `appName?` | `string` | `""` | The app's name, used in the titles ("Install Northwind CRM") and for the letter tile. Empty falls back to labels.fallbackName ("this app"). |
| `appIcon?` | `string \| ReactNode` | `""` | The icon: an image src (shown 48px with 12px corners, use your 192px manifest icon) or an element. Empty draws a tile in the tone with the first letter of appName. |
| `description?` | `string` | `""` | One line under the title: why installing helps. Truncated in the slim banner, wrapped in the sheet and the card. |
| `screenshots?` | `string[]` |  | Sheet variant: a horizontal rail of screenshots (h-72, 9:16, snapping) between the title and the buttons, keyboard scrollable. |
| `variant?` | `"banner" \| "sheet" \| "card"` | `"banner"` | banner: a slim floating card pinned to placement, like a Smart App Banner (on iPhone its Install opens the guide sheet; on a Mac it shows the steps inline). sheet: a bottom sheet with details, a centered dialog from 768px up. card: an inline card you place yourself (a settings page), always visible while the app can be installed. |
| `placement?` | `"bottom" \| "top"` | `"bottom"` | The banner's edge. Bottom sits above a bottom tab bar and a sticky CTA bar (--bottom-bar-offset, --cta-bar-offset) and the home indicator; top sits under the notch, or under --install-prompt-top when you set it (a fixed header). |
| `position?` | `"fixed" \| "absolute"` | `"fixed"` | fixed pins the banner to the viewport (and turns it into a 380px card at the bottom end on desktop); absolute pins it inside the nearest positioned parent (a phone frame, a panel). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The Install button and the letter tile. Keep neutral in an app; brand fits a marketing page. |
| `color?` | `string` | `""` | Any CSS color for the Install button and the letter tile, replacing the tone. |
| `radius?` | `"sm" \| "md" \| "lg"` | `"md"` | Corners of the card and the floating banner: 12, 16 or 24px. |
| `platform?` | `"auto" \| "chromium" \| "ios" \| "macos-safari" \| "unsupported"` | `"auto"` | auto detects it: chromium once the browser has offered an install (beforeinstallprompt), ios on iPhone and iPad in any browser, macos-safari on Safari 17+ for Mac; in-app browsers and everything else are unsupported and render nothing. Force one for demos and QA; a forced chromium without a real event makes Install resolve "unavailable". |
| `delay?` | `number` | `3000` | Milliseconds after it becomes possible before the banner or sheet shows, so it never greets people on arrival. Ignored while open is controlled and by the card. |
| `dismissDays?` | `number` | `14` | How long Not now hides it, in days. 0 hides it for this session only (sessionStorage). |
| `storageKey?` | `string` | `"install-prompt"` | The localStorage key that remembers Not now (a timestamp). Empty keeps it in memory; with storage blocked it lasts the session. |
| `showOnIOS?` | `boolean` | `true` | Show the Add to Home Screen guide on iPhone and iPad. |
| `showOnDesktop?` | `boolean` | `true` | Show it in Chrome and Edge on a computer and the Add to Dock guide in Safari for Mac. |
| `defaultOpen?` | `boolean` | `false` | Open the banner or sheet at once (no delay, a previous Not now ignored). An initially open banner skips its entrance. |
| `open?` | `boolean` |  | Controlled open state of the banner or sheet; pair it with onOpenChange. It still renders nothing where there is no install path. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when it opens after the delay and when it closes (Not now, installed, the sheet dismissed). |
| `onInstall?` | `(outcome: "accepted" \| "dismissed" \| "unavailable") => void` |  | The result of Install in Chromium: accepted, dismissed in the browser dialog (the prompt cannot be shown again until the browser offers it again), or unavailable (no pending event). |
| `onDismiss?` | `() => void` |  | Not now, the ×, Got it in the guide, or the sheet closed by drag, tap outside or Escape. |
| `safeArea?` | `boolean` | `true` | Pad the banner off the notch, the home indicator and the rounded side edges (env(safe-area-inset-*)). |
| `desktop?` | `"dialog" \| "sheet"` | `"dialog"` | What the sheet (and the iPhone guide) becomes at 768px and up: a centered dialog, or the same bottom sheet capped at 420px. |
| `container?` | `HTMLElement \| null` |  | Portal target of the sheet (default document.body). A transformed or contain:layout element keeps the sheet inside it, like a phone frame. |
| `labels?` | `Partial<InstallPromptLabels>` |  | Every string: install "Install", notNow "Not now", title(name) "Install {name}", iosTitle(name) "Add {name} to your Home Screen", iosSteps ["Tap Share in the toolbar", "Scroll and choose Add to Home Screen", "Tap Add"], macTitle(name) "Add {name} to the Dock", macSteps ["Choose File in the menu bar", "Choose Add to Dock"], installed "Installed", fallbackName "this app", close "Close", done "Got it", installedAnnouncement(name) "{name} was installed", screenshots "Screenshots", screenshot(i, n) "Screenshot {i} of {n}", addGlyph "Add", fileGlyph "File". Defaults in DEFAULT_INSTALL_PROMPT_LABELS. |
| `classNames?` | `Partial<Record<"icon" \| "content" \| "title" \| "description" \| "screenshots" \| "steps" \| "step" \| "actions" \| "install" \| "dismiss", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root: the banner's pinned wrapper, the card, or the sheet's content. |

## Parts

### useInstallPrompt (hook)

The same engine without the UI, for your own button (a profile menu item). One store per page listens for beforeinstallprompt and appinstalled from the first call on, so call it (or mount InstallPrompt) in the root layout: the event fires once, early. canInstall is true while the browser prompt is pending (chromium) or the guide applies (ios, macos-safari). Signature: (options?: { platform?: "auto" | InstallPlatform; storageKey?: string; dismissDays?: number }) => { platform: InstallPlatform; canInstall: boolean; isStandalone: boolean; installed: boolean; dismissed: boolean; promptInstall: () => Promise<"accepted" | "dismissed" | "unavailable">; dismiss: () => void; reset: () => void }.

### DEFAULT_INSTALL_PROMPT_LABELS (constant)

The English defaults, to spread and override. Type: InstallPromptLabels.

## Examples

- **iPhone guide**: The only path on iOS: three steps with the icons to look for, and an arrow toward Safari's share button.
- **Install sheet with screenshots**: Chromium: the sheet shows what you get, then Install opens the browser's own dialog.
- **Card in settings**: variant="card" sits in the page and stays while the app can be installed.
- **Top banner, brand tone**: placement="top" under the status bar, for a marketplace's landing screen.
- **Safari on a Mac**: The Add to Dock guide as a card: File, then Add to Dock.
- **Your own button**: useInstallPrompt drives a menu item; the flags are this browser's real state.

## See also

- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Notification Banner](https://wingo-ui.com/components/notification-banner) ([Markdown](https://wingo-ui.com/components/notification-banner.md))
- [Onboarding Carousel](https://wingo-ui.com/components/onboarding-carousel) ([Markdown](https://wingo-ui.com/components/onboarding-carousel.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))

---

Canonical: https://wingo-ui.com/components/install-prompt
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
