1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Install Prompt

Install Prompt

ui

An install button for a web app that appears only when there is genuinely something to install, and knows the difference between an app that cannot be installed, one that can be installed but not by asking, and one somebody is already inside. Reach for it wherever the app is worth keeping rather than visiting: a documentation site or internal tool opened every day, a dashboard checked from a phone, a field-service, delivery, warehouse or point-of-sale app, a recipe, workout or practice timer, a reader, notes or journal app, a kiosk or presentation view, a game, a conference schedule, a ticket or boarding pass, and anything meant to keep working offline. Common asks it answers: "pwa install button react", "beforeinstallprompt react", "add to home screen react", "install prompt component", "useInstallPrompt hook", "a2hs react", "react-pwa-install alternative", "shadcn install button", "custom pwa install button", "beforeinstallprompt not firing", "beforeinstallprompt never fires in react", "install button does not appear", "deferredPrompt is null", "prompt() NotAllowedError pwa", "detect if pwa is installed", "display-mode standalone react", "navigator.standalone ios", "ios add to home screen instructions", "safari pwa install no api", "appinstalled event react", "PWA インストールボタン react", "ホーム画面に追加 react", "beforeinstallprompt 発火しない", "インストールボタンが出ない". Official shadcn/ui has nothing of the kind, and the figure is measured rather than remembered: its catalogue today lists 65 entries of which 62 are fetchable (code-block, questionnaire and trace 404) for 223,287 bytes of component source, and in all of it beforeinstallprompt, appinstalled, display-mode, standalone, navigator.standalone, manifest, pwa and even the bare words install and prompt are every one of them zero hits. So an agent asked for this writes it from scratch, and the version it writes has one bug in it that local testing will not show. beforeinstallprompt fires once, early, and never replays: Chromium dispatches it as soon as its own heuristics are satisfied, which on a warm cache is routinely before a React bundle has been parsed, let alone hydrated — so a listener added in useEffect is not late by a frame, it is late by the whole load. It passes in development, where the bundle arrives first, and fails in production, which is the single most common report about PWA install buttons: "it never shows up on my machine, but it does on yours". This component registers at module scope, moving the listener earlier by however long hydration takes, and exports earlyCaptureScript() — a few lines to inline in <head> ahead of the bundle — for the loads where nothing inside the bundle can be early enough, because the event has already fired by the time the bundle exists. The captured event is parked on window rather than in module state, so a page that somehow loads two copies of the file still shares one event instead of each copy believing there is nothing to install. Three more things separate it from the twenty-line version. Availability is four-valued — available, instructions, installed, unavailable — because "cannot install" and "can install, but not by asking" need different interfaces, and collapsing the second into the first is how an iPhone gets shown nothing at all on the platform where adding to the home screen matters most: iOS has no beforeinstallprompt at all, routes the whole thing through the share sheet, and so is given written steps instead — the same "design the missing environment as the real one" shape as share-button and fullscreen-button. Already-installed is read from matchMedia across all four installed display modes rather than standalone alone, because a manifest can ask for fullscreen or minimal-ui and an installed desktop app drawing into its own title bar reports window-controls-overlay — a check against standalone alone offers to install the app to somebody already running it — while iOS spells it navigator.standalone and has never implemented the media feature. And prompt() is honoured only during a user gesture and only once per event, so it is called as the first thing in the handler with nothing awaited in front of it: an await first, reading a flag or asking an API whether to offer this, is what turns a working button into NotAllowedError. The event is marked spent before anything is awaited, because a second click can land while userChoice is still unresolved. The first render is always the empty one, on the server and the client alike, so there is no hydration mismatch and nothing flashes in and back out again — reading matchMedia or the parked event during render instead is the same bug wearing a different hat. The component returns null whenever there is nothing to offer, so it can be left mounted where it belongs in the layout instead of being wrapped in a condition the page has to work out for itself. useInstallPrompt() is exported for a button of your own design, handing back status and promptToInstall(), with isRunningInstalled() and isIosLike() alongside it; onOutcome reports "accepted", "dismissed" or "unavailable" for analytics, and the manual steps are a prop, so they can be re-worded, translated or pointed at a different browser. It deliberately does not remember a refusal — whether somebody who said no yesterday should be asked today is a product decision with no safe default, and a shopping site and an internal tool want opposite answers — so record it through onOutcome, or compose it with announcement-bar's remembered dismissal. Within pulld it sits with the components that handle what the browser is doing behind the app: wake-lock-toggle holds a resource the browser can take back silently, network-status works out whether the connection is really there, geolocation-button handles a permission that can be refused for good, and this one handles the single capability the page cannot ask for twice. The instructions panel stays in the tree and is hidden with the attribute rather than left out, because aria-controls has to point at an element that exists; the toggle carries aria-expanded, both icons are aria-hidden, and the focus ring is focus-visible. Every colour is a shadcn token, so light and dark follow on their own, and its only dependency is lucide-react for the two icons.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/install-prompt.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
  • lucide-react
    components/ui/install-prompt.tsx