# pwa-install: a web component that replaces the missing install dialog

> pwa-install is a small Lit based web component that shows its own installation dialog for progressive web apps, covering the browsers that ship no native prompt and the in-app browsers that cannot show one. It suits teams shipping a PWA to iOS, Safari or an embedded web view, and it does not change how the browser actually installs the app.

**khmyznikov/pwa-install** — Installation dialog for Progressive Web Application. Provides a more convenient user experience and fixes the lack of native dialogs in some browsers.

- Repository: https://github.com/khmyznikov/pwa-install
- Website: https://khmyznikov.com/pwa-install/
- Stars: 961 · Forks: 119
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/khmyznikov-pwa-install

## The prompt that never appears on iOS and in web views

Installing a progressive web app is easy where the browser cooperates and impossible where it does not. Chromium browsers expose a native install dialog. Safari on iOS and iPadOS does not, Firefox and Opera do not, and neither do the in-app browsers that live inside apps such as Instagram or Telegram, where the only way to add the site to the home screen is for the user to follow a set of manual steps in a share sheet.

pwa-install exists to fill that gap. The README describes it as an installation dialog for Progressive Web Applications and an Add to Home Screen or Dock dialog for web apps, and states that it offers an enhanced user experience and addresses the absence of native dialogs in certain browsers, naming Safari, Firefox and Opera among them.

The audience is therefore front-end teams whose product is a PWA and whose users arrive somewhere other than a desktop Chromium browser. The primary language is TypeScript, the licence is MIT, the package is published on npm as @khmyznikov/pwa-install, and the project site at khmyznikov.com/pwa-install/ doubles as the demo and the gallery of screenshots for iOS, iPadOS, macOS, Android, Chrome and the in-app browsers.

## One web component, any framework, no polyfill

The implementation is a custom element built with Lit, and the README is firm about the integration path: use it as a web component with any modern framework, and no polyfill is required. The element is registered by importing the package, and the component itself is a single tag in your markup:

```html
<pwa-install></pwa-install>
```

That decision is what makes it usable across a stack. The README links samples for React up to 18, React 19 and later, Next.js 15 with React 19, Angular and Svelte, and says it should work with any other modern framework or with plain JavaScript as a web component. The repository topics list chrome, ios, safari, lit, lit-element, react and web-components, which is the same list.

The React story has a wrinkle worth knowing about. The package exports map in package.json shows the main entry plus a separate react-legacy export with its own type declarations, alongside the dist path for the same file, and the README links a separate polyfill sample for React 18 and earlier. The split exists because React versions before 19 did not support custom elements properties and events in the same way, so the legacy build adapts the element for them.

The size is modest. The README states a 39kB brotli compressed bundle, which is what you ship to every visitor of the page that includes the element.

## The Web Install API path and the legacy fallback

On Chromium the component prefers the modern route. The README states that on supported Chromium browsers the component uses the Web Install API automatically after fetching the configured manifest, and the release notes highlight Web Install API support for chromium browsers v155 and above.

The exact signature it calls depends on three conditions, and the README spells them out because they decide whether the install succeeds. When manifest-url matches the current document's linked manifest, when manifest-id is omitted, and when the fetched manifest declares an id, the component uses the current-document navigator.install() signature. A different manifest or an explicit manifest-id switches it to navigator.install({ manifest, manifestId }). Relative manifest URLs are resolved against the current document, and manifest-id is optional when the manifest declares a non-empty id.

Underneath that, the component keeps intercepting and retaining the beforeinstallprompt event as a legacy fallback, so browsers that have not shipped the newer API still work through the older mechanism.

This split matters for the browsers that motivated the project. For iOS, Firefox, Opera and in-app browsers there is no native prompt to intercept, so the component shows instructions instead. The README lists in-app browser instruction support and support for iOS, iPadOS and macOS 27 or newer for a native look and feel, and the gallery shows the default dialog, an instructions variant and an app gallery view for each platform.

## Parameters that decide whether the dialog appears at all

The element takes a set of attributes that control its behaviour, and the first half of the list covers the manual and disabling modes:

```html
<pwa-install
  manual-apple
  manual-chrome
  disable-chrome
  disable-close
  use-local-storage

  install-description="Custom call to install text"
  disable-install-description
  disable-screenshots
  disable-screenshots-apple
  disable-screenshots-chrome
  manual-how-to

  disable-android-fallback
```

The remaining attributes describe the manifest and the copy:

```html
<pwa-install
  manifest-url="/manifest.json"
  manifest-id="https://example.com/app-id"
  name="PWA"
  description="Progressive web application"
  icon="/icon.png">
</pwa-install>
```

The documented behaviour is worth reading carefully, because these are not just cosmetic switches. manual-apple and manual-chrome let you control the dialog manually with showDialog(). manual-chrome applies to all non-Apple devices, and the README clarifies two edge cases: Chrome on iPad and iOS falls into the manual-apple category, while Firefox on Android falls into manual-chrome. disable-chrome completely disables the custom logic and interception for Chromium browsers so the browser's own logic works. disable-close makes the dialog impossible for the user to close. use-local-storage stores the preference to ignore the prompt in long-lived storage, so the user is not prompted again unless the application data is cleared. disable-android-fallback disables the install instructions dialog for non-Chrome browsers on Android. manual-how-to shows the instructions immediately and disables screenshots on Apple devices.

Two notes from the README will save you an hour. It advises making a good manifest file and not using the name, description and icon parameters, and it says boolean attributes need to be removed to act like false, since in HTML a bare attribute is a string.

## Installing it and putting it in a page

The package is on npm, and installation is a single command:

```bash
npm i @khmyznikov/pwa-install
```

The README also points at unpkg and esm.sh as alternatives for loading the module without a build step. Importing it registers the custom element:

```js
import '@khmyznikov/pwa-install';
```

TypeScript users need two extra type packages visible in the compiler options, because the component uses the Chromium installation event types and the web app manifest types:

```json
"compilerOptions": {
  "moduleResolution": "Bundler",
  "types": ["dom-chromium-installation-events", "web-app-manifest"]
}
```

With the module resolution set to Bundler and those types registered, the element can be placed in a template and it will resolve the manifest and show the dialog. In a React application the equivalent is to render the tag, and the README links working samples for React 18 and earlier with a polyfill, React 19 and later, Next.js 15 with React 19, Angular and Svelte.

The result to look for in a browser is the component's own dialog, with the platform-specific instructions where no native prompt exists and the browser's own install flow where one does.

## Localization and a build pipeline that ships several formats

Translation and localization are supported, and the repository shows how seriously that is taken. There is an i18n/ directory at the top level and a lit-localize.json file, and the build script starts by extracting and building the localized messages before producing any bundle. The package.json scripts show localize:extract and localize:build running ahead of the actual compilation, which means the localized strings are compiled into the output rather than fetched at runtime.

The bundling itself is done with webpack and produces several artefact shapes, because the consumers differ. The exports map points the default entry at dist/pwa-install.es.js as an ES module, the main field points at dist/pwa-install.bundle.js, the types field at dist/types/index.d.ts, and the react-legacy entry has its own JavaScript and declaration files. The build chain runs typecheck, then webpack for the production bundle, the production module bundle and the production UMD bundle, then a separate webpack configuration for the React build, then the TypeScript declarations, then a custom elements manifest analysis step, and finally a postbuild script.

The custom elements manifest configuration and the generated custom-elements.json are there for tooling, and the files array in package.json publishes dist, the docs entry point and parts of src, including src/utils.ts and the src/types directory, so the TypeScript types and the utility helpers are importable from the package rather than only from the repository.

## Where pwa-install is the wrong tool

The first limitation is that it does not install anything by itself on the platforms it targets. On iOS, Firefox, Opera and inside an in-app browser, the user still has to follow instructions manually, so the component is a better explanation of a manual process, not an automation of it. If your metric is installs per session rather than understanding, the ceiling is the platform's.

Second, the browser's own prompt is usually better where it exists, and the component is aware of that. The disable-chrome attribute exists precisely to turn off the custom logic on Chromium so the built-in behaviour can work, and the Web Install API path in the modern Chromium versions is the one that produces a genuine install rather than an explanation.

Third, the component is a UI decision with a maintenance cost. It has to keep pace with a platform that changes the install story between releases, and the manifest rules the component relies on are the same ones the browser enforces, so a manifest change can break the dialog silently. The version numbering reflects a young project: v0.6.2 on 2026-01-05, v0.6.4 on 2026-06-26 and v0.7.0 on 2026-09-14, with the last push on the same day as that release.

Fourth, the browser support matrix is uneven. Some attributes are platform specific, manual-chrome covers all non-Apple devices while Chrome on iOS is treated as Apple, and disable-android-fallback exists because the Android fallback instructions do not apply everywhere. Code that assumes a single dialog shape across platforms will be wrong.

## Against a custom install prompt of your own

The alternative is to write the dialog yourself, which is what most teams do before finding this component. The work is not the visual design, it is the platform matrix: detecting whether the browser is Safari on iOS, an in-app browser on Android, Firefox, Opera or Chromium, deciding in each case whether to trigger the native prompt, to show manual instructions, or to stay quiet, and then remembering to suppress the prompt for users who already dismissed it.

The difference in approach is where that matrix lives. A hand-written prompt puts the detection logic, the artwork for each platform, the translations and the dismissal memory in your application, where they are maintained by your team and where they compete with the rest of your front end. pwa-install moves all of it into a maintained package, gives you one attribute to place and a set of attributes to tune, and ships the translations and the per-platform screenshots with it.

The cost is that you accept the component's design. The dialog, the wording, the iconography and the platform-specific instruction steps are the library's, and the parameters are a limited vocabulary for adjusting them rather than an open template system. There is no styling API described in the README, so if the dialog has to match a brand system exactly, a custom component is the only route.

There is also a middle path the parameters invite: on browsers that do support the native prompt, set disable-chrome and let the browser handle the install, and use the component only where there is no native prompt at all. That keeps the custom UI to the platforms that need it and avoids shadowing a dialog the user already trusts.

## Conclusion

Adopt pwa-install if your web app is installed from a surface that shows no native prompt, such as iOS Safari, Firefox, Opera or an Instagram style in-app browser, and if you can show instructions without hijacking the user's intent. Do not adopt it on a Chromium browser where the built-in install UI already works, because the README provides disable-chrome precisely to hand control back to the browser there. Verify first that your manifest is complete, since the README advises against passing name, description and icon parameters and instead expects a good manifest file, and note the component is at version 0.7.0 with the last push on 2026-09-14.

## FAQ

### What is pwa-install used for?

It provides an installation dialog for progressive web apps and an Add to Home Screen or Dock dialog for web apps, covering browsers that ship no native install prompt such as Safari, Firefox and Opera, as well as in-app browsers.

### How do I install pwa-install in my project?

Run npm i @khmyznikov/pwa-install, then import the package so the custom element is registered, with import '@khmyznikov/pwa-install'. The README also points at unpkg and esm.sh if you would rather load the module without a build step.

### Does pwa-install work with React and other frameworks?

Yes, it is a web component and the README links samples for React up to 18 with a polyfill, React 19 and later, Next.js 15 with React 19, Angular and Svelte, and states that no polyfill is required with any modern framework or plain JavaScript.

### How do I stop pwa-install from showing its own dialog on Chrome?

Set the disable-chrome attribute. The README describes it as completely disabling the custom logic and interception for Chromium browsers, allowing the browser's built-in logic to work.

### Can I show the pwa-install dialog myself instead of automatically?

Yes. The manual-apple and manual-chrome attributes let you control the dialog manually with showDialog(), with manual-chrome covering all non-Apple devices and manual-apple covering Apple ones.

### What TypeScript configuration does pwa-install need?

The README shows a compilerOptions block with moduleResolution set to Bundler and the types array containing dom-chromium-installation-events and web-app-manifest, since the component relies on the Chromium installation event types and the web app manifest types.

## Sources

- [khmyznikov/pwa-install on GitHub](https://github.com/khmyznikov/pwa-install)
- [License: MIT](https://github.com/khmyznikov/pwa-install/blob/main/LICENSE)
- [Project website](https://khmyznikov.com/pwa-install/)
- [README](https://github.com/khmyznikov/pwa-install/blob/main/README.md)
- [Releases](https://github.com/khmyznikov/pwa-install/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/khmyznikov-pwa-install
