Notify
@ailuracode/alpine-notify
Thin wrapper around the Web Notifications API via the $notify magic. Handles unsupported browsers and permission states without throwing.
Install
pnpm add @ailuracode/alpine-notify @ailuracode/alpine-core @ailuracode/alpine-permissions alpinejsQuick start
import Alpine from "alpinejs";import notify from "@ailuracode/alpine-notify";
Alpine.plugin(notify);Alpine.start();Copy the bundled service worker to your site root (or another same-origin path):
cp node_modules/@ailuracode/alpine-notify/dist/notify-sw.js public/notify-sw.jsThe plugin registers /notify-sw.js automatically. Use a custom path when needed:
Alpine.plugin( notify({ serviceWorkerUrl: "/assets/notify-sw.js", }),);Avoiding name collisions
If your application already owns a $notify magic or another toolkit plugin registers on that name, rename the integration surface without touching the controller:
Alpine.plugin(notifyPlugin({ magicKey: "alerts" })); // → $alertsThe exposed constant DEFAULT_NOTIFY_MAGIC_KEY keeps the rename discoverable from TypeScript.
Magic API
| Member | Type | Description |
|---|---|---|
isSupported |
boolean (getter) |
true when notifications can be shown in this environment |
requiresHomeScreenInstall |
boolean (getter) |
true on iOS/iPadOS Safari tabs that need a Home Screen install |
permission |
NotificationPermission (getter) |
granted, denied, or default |
requestPermission() |
Promise<NotificationPermission> |
Prompts the user when permission is default |
send(title, options?) |
Notification | null |
Creates a desktop notification synchronously |
sendAsync(title, options?) |
Promise<Notification | null> |
Preferred on mobile; uses a service worker when needed |
sendIfPermitted(title, options?) |
Notification | null |
Same as send — explicit intent in templates |
sendIfPermittedAsync(title, options?) |
Promise<Notification | null> |
Same as sendAsync |
close(notification) |
void |
Closes a notification safely |
Use getters without parentheses in templates: $notify.isSupported, $notify.permission.
All methods except requestPermission() are synchronous. Nothing throws when notifications are unavailable.
Usage examples
Simple notification
$notify.send("Hello");With options
$notify.send("Order completed", { body: "Your payment was successful.", icon: "/logo.png",});Request permission first
<button x-show="$notify.isSupported && $notify.permission === 'default'" @click="await $notify.requestPermission()"> Enable notifications</button>await $notify.requestPermission();await $notify.sendAsync("You are subscribed");Only notify when already allowed
$notify.sendIfPermitted("Background job finished");Close programmatically
<div x-data="{ note: null }" @job-complete.window="note = $notify.sendIfPermitted('Done')"> <button x-show="note" @click="$notify.close(note); note = null"> Dismiss </button></div>Feature detection in templates
<div x-show="!$notify.isSupported && !$notify.requiresHomeScreenInstall"> Notifications are not supported in this browser.</div>
<div x-show="$notify.requiresHomeScreenInstall"> Add this site to your Home Screen on iPhone or iPad to enable notifications.</div>
<div x-show="$notify.isSupported && $notify.permission === 'denied'"> Notifications are blocked. Enable them in browser settings.</div>Behavior
- Unsupported browsers —
isSupportedisfalse,permissionreturnsdenied,send/sendIfPermittedreturnnull. - iOS/iPadOS Safari tabs —
requiresHomeScreenInstallistrue; notifications only work after the user adds the site to the Home Screen and opens it from there. - Android and mobile Chrome —
new Notification()is not available; the plugin usesServiceWorkerRegistration.showNotification()via the bundlednotify-sw.js. - Denied permission —
Notificationis never constructed; methods returnnullordeniedwithout throwing. - Default permission —
sendreturnsnulluntil the user grants access viarequestPermission(). - Granted permission — use
sendAsync()on mobile andsend()on desktop.
The plugin does not render UI, manage toast stacks, or persist preferences. Use your own components for in-app messaging and permission UX.
Browser compatibility
| Environment | Notes |
|---|---|
| Chrome, Edge, Opera (desktop) | Supported in secure contexts via new Notification() |
| Firefox (desktop) | Supported in secure contexts |
| Safari (macOS 16.4+) | Supported in secure contexts |
| Chrome (Android) | Requires the bundled service worker and sendAsync() |
| Safari (iOS / iPadOS) | Home Screen web app only; regular Safari tabs cannot receive notifications |
| HTTP (non-localhost) | Blocked — requires HTTPS |
| Web Workers / Service Workers | This plugin targets window / Alpine templates in the main document |
Always check isSupported, requiresHomeScreenInstall, and permission before showing permission prompts or assuming notifications will appear.
Unified permissions adapter
Register with @ailuracode/alpine-permissions for a normalized snapshot across capabilities:
import { permissionsPlugin } from "@ailuracode/alpine-permissions";import { createNotificationPermissionAdapter } from "@ailuracode/alpine-notify";
Alpine.plugin( permissionsPlugin({ adapters: [createNotificationPermissionAdapter()], }));Registry key: notifications. See permissions.md.
TypeScript
/// <reference types="@types/alpinejs" />/// <reference types="@ailuracode/alpine-notify" />Or import the plugin module:
import notify from "@ailuracode/alpine-notify";Individual helpers are also exported for non-Alpine use:
import { createNotifyMagic, isNotifySupported, sendNotification,} from "@ailuracode/alpine-notify";Design notes
- Magic, not store — notifications are one-off actions, not shared reactive state.
- Fail silent — returning
nullkeeps Alpine expressions and event handlers simple. - No UI coupling — framework-agnostic; pair with your own toast or banner components for in-page feedback.
License
MIT
