Simple native Android features for web apps packaged with the PWA to APK Builder: back gesture, vibration, notifications, native storage, sharing and more. No Capacitor knowledge needed. The same code also runs in a normal browser using web fallbacks.
Include the script in your HTML, either from this website:
<script src="https://finnfreitag.com/tools/pwabuilder/appnative-1.0.js"></script>or download it and put it into your project (recommended for ZIP uploads, so your app also works offline):
<script src="appnative-1.0.js"></script>Then use the global AppNative object:
AppNative.showToast("Hello from Android!");
AppNative.vibrate(200);
AppNative.onBack(() => {
if (menuOpen) { closeMenu(); return true; }
});window.Capacitor.Plugins for the plugins included in the APK (App, Haptics, Toast, LocalNotifications, Preferences, Share, Filesystem, Clipboard, Network, Browser).appnative-1.0.js), so updates will never break existing apps.AppNative.isNativeApp(): booleanReturns true when the page runs inside an APK built with the PWA Builder, false in a normal browser.
if (AppNative.isNativeApp()) {
document.body.classList.add("in-app");
}AppNative.showToast(text: string, options?: { long?: boolean }): Promise<void>Shows a short message at the bottom of the screen. Set long: true to show it longer (about 3.5 s instead of 2 s).
AppNative.showToast("Saved!");
AppNative.showToast("Level complete – well done!", { long: true });AppNative.vibrate(pattern: number | number[]): Promise<void>Vibrates for the given number of milliseconds. An array is a pattern of vibrate and pause times: [vibrate, pause, vibrate, …].
AppNative.vibrate(200);
AppNative.vibrate([100, 50, 100]); // buzz – pause – buzzAppNative.haptic(style: "light" | "medium" | "heavy"): Promise<void>A short haptic tap, e.g. for button presses or game events. Feels more subtle than vibrate().
button.addEventListener("click", () => AppNative.haptic("light"));AppNative.scheduleNotification({ at: Date, title: string, text: string, repeat?: "minute" | "hour" | "day" | "week" }): Promise<number | null>Schedules a notification for the given date and resolves to its id (use it with cancelNotification). On first use, the user is asked for notification permission; if they deny it, the promise resolves to null. With repeat, the notification fires again every minute / hour / day / week at the time taken from at, starting with the next matching time.
const id = await AppNative.scheduleNotification({
at: new Date(Date.now() + 60_000),
title: "Timer",
text: "Your 1 minute timer is done!",
});
// Every day at 08:00
const tomorrow8 = new Date();
tomorrow8.setDate(tomorrow8.getDate() + 1);
tomorrow8.setHours(8, 0, 0, 0);
AppNative.scheduleNotification({ at: tomorrow8, title: "Daily Maze", text: "A new maze is waiting!", repeat: "day" });AppNative.cancelNotification(id: number): Promise<void>Cancels a scheduled (or repeating) notification by the id returned from scheduleNotification.
AppNative.cancelNotification(id);AppNative.showNotification(title: string, text: string): Promise<void>Shows a notification right away. Asks for permission on first use.
AppNative.showNotification("Download finished", "Your level pack is ready.");AppNative.onBack(handler: () => boolean | void): () => voidReacts to the Android back gesture / back button. Return true from the handler if you handled the event (e.g. closed a menu). If no handler returns true, the default happens: go back in the page history, or close the app if there is no history. Handlers registered later run first, so nested dialogs work naturally. Returns a function that removes the handler.
const off = AppNative.onBack(() => {
if (menuOpen) {
closeMenu();
return true; // handled, don't leave the page
}
});
// later: off();AppNative.exitApp(): Promise<void>Closes the app.
quitButton.onclick = () => AppNative.exitApp();AppNative.onPause(fn: () => void): () => voidCalled when the app goes to the background (home button, app switch, screen off). Good place to pause games or save state. Returns a function that removes the listener.
AppNative.onPause(() => game.pause());AppNative.onResume(fn: () => void): () => voidCalled when the app comes back to the foreground. Returns a function that removes the listener.
AppNative.onResume(() => refreshData());AppNative.onNetworkChange(fn: (online: boolean) => void): () => voidCalled whenever the device goes online or offline. Returns a function that removes the listener.
AppNative.onNetworkChange(online => {
banner.hidden = online;
});AppNative.storage.set(key: string, value: any): Promise<void>
AppNative.storage.get(key: string): Promise<any | null>
AppNative.storage.remove(key: string): Promise<void>Saves values permanently on the device. Values are stored as JSON, so numbers, strings, booleans, arrays and objects come back with the same type. get resolves to null if the key doesn't exist.
await AppNative.storage.set("highscore", 42);
await AppNative.storage.set("settings", { sound: true, level: 3 });
const score = await AppNative.storage.get("highscore"); // 42
await AppNative.storage.remove("settings");AppNative.share({ title?: string, text?: string, url?: string, image?: Blob | File | HTMLCanvasElement | string }): Promise<boolean>Opens the system share sheet. You can share text, a link, an image, or all of them together. image can be a Blob/File, a <canvas> element (great for game screenshots), a data URL or an image URL. Resolves to true if the share sheet was used, false if it was cancelled or isn't available.
AppNative.share({ title: "Daily Maze", text: "I solved today's maze in 42s!", url: "https://example.com" });
// Share a screenshot of a canvas
AppNative.share({ text: "My new highscore!", image: document.querySelector("canvas") });
// Share an image file from your project
AppNative.share({ image: "images/badge.png" });AppNative.copyToClipboard(text: string): Promise<boolean>Copies text to the clipboard. Resolves to true on success.
await AppNative.copyToClipboard(inviteCode);
AppNative.showToast("Copied!");AppNative.openExternal(url: string): Promise<void>Opens a link outside of your app, so the user doesn't navigate away from it inside the app's WebView.
AppNative.openExternal("https://finnfreitag.com");AppNative.keepScreenOn(enabled: boolean): Promise<void>Prevents the screen from turning off while enabled, e.g. for timers, recipes or videos.
AppNative.keepScreenOn(true);
// ...
AppNative.keepScreenOn(false);AppNative.setStatusBar({ color?: string, lightIcons?: boolean }): Promise<void>Sets the color behind the Android status bar (and navigation bar) and whether its icons are light (for dark colors) or dark. If lightIcons is left out, it is chosen automatically from the color. Note: if your page uses viewport-fit=cover, your content is drawn behind the status bar and you color that area yourself in CSS (use var(--safe-area-inset-top)).
AppNative.setStatusBar({ color: "#000000", lightIcons: true });
AppNative.setStatusBar({ color: "#ffffff" }); // dark icons chosen automatically