Feedback
InstallPrompt
A quiet offer to install the web app, shown only where the browser can install it (Chromium on Android and desktops), never while it already runs installed, and not again for two weeks after Not now. Install opens the browser's own install dialog. It renders nothing otherwise, so it can sit in a layout unconditionally. The browser's install event is caught as soon as the module loads, so an offer is never missed because the page mounted late. When Install or Not now takes the card away while it has focus, focus goes back where it was before the card, or to the next control on the page when that is gone.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
In its source: app/root.tsx
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/install-prompt#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/install-promptIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/install-prompt#v1.2.13 ourostack/ouro-ui/install-prompt-tests#v1.2.13import { InstallPrompt } from "@/components/ouro/install-prompt/install-prompt";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<InstallPrompt className="mt-6" />Use instead when
- Use
useInstallPromptinstead to offer installing from a control of your own, such as a menu item.
Props
Type: InstallPromptProps.
className- Type:
string - Layout only: margins, width (
mt-6,max-w-md). Appearance classes warn in development. description- Type:
ReactNode - Default:
"Add it to your home screen or dock and open it like any other app." - What installing gives the person.
dismissLabel- Type:
string - Default:
"Not now" - The label of the button that puts the offer away.
headingLevel- Type:
2 | 3 | 4 | 5 | 6 - Default:
2 - The title's heading level.
installLabel- Type:
string - Default:
"Install" - The label of the button that opens the browser's install dialog.
media- Type:
ReactNode - A picture across the top, such as the app on a phone.
onInstall- Type:
((outcome: "accepted" | "dismissed") => void) - Called with how the person answered the browser's install dialog.
remindAfterDays- Type:
number - Default:
14 - Days before offering again after Not now.
storageKey- Type:
string - Default:
"ouro-install-dismissed" - The
localStoragekey that keeps when the person said Not now. title- Type:
string - Default:
"Install the app" - The offer's title.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
onAccept: Not a prop. Ouro UI saysonInstall:<InstallPrompt onInstall={(outcome) => …} />.open: Not a prop. The offer shows itself when the browser can install the app; there is nothing to open.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: InstallPrompt merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
useInstallPrompt()
Whether the browser offers to install this web app, and the means to offer it at a good moment rather than when the browser chooses: install() opens the browser's own install dialog, dismiss() puts the offer away for two weeks. It is available only where the browser fires beforeinstallprompt (Chromium on Android and desktops; not Safari or Firefox), and never while the app already runs installed.
import { useInstallPrompt } from "@/lib/ouro/use-install-prompt";Example
const { available, install } = useInstallPrompt();
if (available) return <Button onClick={install}>Install the app</Button>;Use instead when
- Use
InstallPromptinstead for the whole offer, a card with Install and Not now.
Parameters
{ remindAfterDays = 14, storageKey = "ouro-install-dismissed" }- Type:
InstallPromptOptions {}
Returns
InstallPromptState
Types
InstallPromptOptions
Options for useInstallPrompt.
interface InstallPromptOptions {
/** Days to wait before offering again after the person says not now. @default 14 */
remindAfterDays?: number | undefined;
/** The `localStorage` key that keeps when the person last said not now. @default "ouro-install-dismissed" */
storageKey?: string | undefined;
}InstallPromptState
What useInstallPrompt returns.
interface InstallPromptState {
/** Whether to offer installing now: the browser can install the app, it is not installed, and the person has not said not now lately. */
available: boolean;
/** Whether the app runs installed (standalone) or was installed from this page. */
installed: boolean;
/** Opens the browser's install dialog and resolves how the person answered. */
install: () => Promise<"accepted" | "dismissed" | "unavailable">;
/** Hides the offer and remembers it for `remindAfterDays`. */
dismiss: () => void;
}Claim sources
- layout-classes: test:registry/ouro/components/ouro/install-prompt/install-prompt.test.tsx::InstallPrompt > lets app layout classes win
verify:testExists(registry/ouro/components/ouro/install-prompt/install-prompt.test.tsx::InstallPrompt > lets app layout classes win)