Skip to content

Notify

@ailuracode/alpine-notify

Thin wrapper around the Web Notifications API via the $notify magic. Handles unsupported browsers and permission states without throwing.

Install

Terminal window
pnpm add @ailuracode/alpine-notify @ailuracode/alpine-core @ailuracode/alpine-permissions alpinejs

Quick 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):

Terminal window
cp node_modules/@ailuracode/alpine-notify/dist/notify-sw.js public/notify-sw.js

The 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" })); // → $alerts

The 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 browsersisSupported is false, permission returns denied, send / sendIfPermitted return null.
  • iOS/iPadOS Safari tabsrequiresHomeScreenInstall is true; notifications only work after the user adds the site to the Home Screen and opens it from there.
  • Android and mobile Chromenew Notification() is not available; the plugin uses ServiceWorkerRegistration.showNotification() via the bundled notify-sw.js.
  • Denied permissionNotification is never constructed; methods return null or denied without throwing.
  • Default permissionsend returns null until the user grants access via requestPermission().
  • Granted permission — use sendAsync() on mobile and send() 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 null keeps 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