npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wcstack/notification

v1.31.0

Published

Declarative desktop-notification component for Web Components. Framework-agnostic Notifications API wrapper via wc-bindable-protocol, with Service Worker support.

Readme

@wcstack/notification

🤖 AI coding agents: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository README and AGENTS.md, then use the wcstack-app skill.

@wcstack/notification is a headless desktop-notification component for the wcstack ecosystem.

It is not a visual UI widget. It is an async primitive node that turns the Notifications API into reactive state and a state-driven command — the same way @wcstack/geolocation turns the device's location into reactive state.

With @wcstack/state, <wcs-notify> can be bound directly through path contracts:

  • command surface: request, notify, close, closeAll
  • input surface: notice (reactive show), mode, body, icon, badge, tag, lang, dir, require-interaction, silent, renotify, manual
  • output state surface: permission, granted, denied, prompt, unsupported, error, errorInfo, clicked, closed, shown

@wcstack/notification follows the CSBC (Core / Shell / Binding Contract) architecture:

  • Core (NotificationCore) handles permission, showing (constructor or Service Worker), and click relaying
  • Shell (<wcs-notify>) connects that to DOM attributes, the reactive notice input, and lifecycle
  • Binding Contract (static wcBindable) declares observable properties, inputs, and commands

Why this exists — both directions in one tag

Most wcstack IO nodes lean one way: <wcs-permission> only watches (it has no commands, because the Permissions API has no request()); <wcs-speak>/<wcs-listen> split a duo across two tags. The Notifications API is different — it is genuinely bidirectional in a single API:

  • show is a command (state → element): notify(title, options).
  • click / close / show are events (element → state): the user interacting with the OS notification.

So <wcs-notify> is the first wcstack node where the command-token (show) and event-token (click) directions live together in one tag. And unlike <wcs-permission>, the Notifications API does have Notification.requestPermission(), so this node is self-contained: it both requests/monitors the permission and shows notifications.

Secure context required. The Notifications API only works in a secure context (HTTPS, or localhost). Where it is absent, <wcs-notify> reports permission = "unsupported" instead of throwing. Requesting permission and showing also typically require a user gesture — firing notify from a timer may show nothing.

Install

npm install @wcstack/notification

Quick Start

1. Ask, then show — from state

<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/notification/auto"></script>

<wcs-state>
  <script type="module">
    export default {
      $commandTokens: ["request", "notify"],
      $eventTokens: ["opened"],
      ask()  { this.$command.request.emit(); },
      send() { this.$command.notify.emit("New message", { body: "Tap to open", tag: "chat", data: { room: 7 } }); },
      $on: {
        opened: (state, event) => { console.log("clicked", event.detail); }, // { tag, data, action }
      },
    };
  </script>
</wcs-state>

<wcs-notify data-wcs="
  command.request: $command.request;
  command.notify:  $command.notify;
  eventToken.clicked: opened
"></wcs-notify>

<button data-wcs="onclick: ask">Allow notifications</button>
<button data-wcs="onclick: send">New message</button>

The positional args of notify.emit(title, options) pass straight through to notify(title, options) — the same argument-forwarding contract used by <wcs-speak>/<wcs-fetch>.

2. Reactive notice vs imperative notify

<!-- reactive: shows whenever the bound value *changes* (same-value guard). -->
<wcs-notify data-wcs="notice: statusMessage | debounce(1000)"></wcs-notify>

notice is the declarative counterpart of notify: writing a changed value shows it; an identical write is suppressed. The imperative notify command fires every call (even the same text). Auto-firing a notification on every state change risks spam, so debounce the bound source and prefer a tag so the OS de-dups.

3. Permission as bindable state

<wcs-notify data-wcs="permission: notifyPerm; granted: canNotify"></wcs-notify>

<!-- one boolean, straight from the node -->
<div data-wcs="hidden: canNotify">Allow notifications to get alerts.</div>

permission is "prompt" / "granted" / "denied" / "unsupported". The Notifications API's own "default" is normalized to "prompt", so this node shares the exact four-value surface of @wcstack/permission / @wcstack/geolocation.

4. Reading the click

<wcs-notify data-wcs="command.notify: $command.notify; eventToken.clicked: opened"></wcs-notify>

clicked / closed / shown carry { tag, data, action }. tag identifies the notification (your options.tag, or a generated wcs-<n> when omitted); data is whatever you passed in options.data; action is the Service Worker action-button id (always "" for the constructor backend).

See examples/state-notification-chat for the full demo.

Service Worker / mobile

new Notification() works on desktop only. On Android Chrome it throws, and ServiceWorkerRegistration.showNotification() is required. <wcs-notify> picks the backend per mode:

| mode | Behavior | | ------------- | --------------------------------------------------------------------------------- | | auto (default) | Try the Notification constructor; on a TypeError (mobile), fall back to the SW. | | constructor | Constructor only; a TypeError surfaces as an error (no fallback). | | sw | Always ServiceWorkerRegistration.showNotification(). |

The SW's notificationclick fires inside your Service Worker, which this package cannot inject into. Import the one-line helper so clicks relay back to the page:

// your sw.js
import { wireNotificationClicks } from "@wcstack/notification/sw";
wireNotificationClicks();

It relays each click over BroadcastChannel("wcs-notify") (primary) and clients.postMessage (fallback); NotificationCore on the page de-dups the two transports and emits wcs-notify:click.

Attributes / Inputs

| Attribute | Type | Default | Description | | --------------------- | ------- | ------- | --------------------------------------------------------------------------- | | mode | string | auto | Show backend: auto / sw / constructor. | | body | string | "" | Notification body text. | | icon | string | "" | Icon URL. | | badge | string | "" | Badge URL (monochrome, mobile). | | tag | string | "" | Notification tag (the OS replaces a notification with the same tag). | | lang | string | "" | Language tag. | | dir | string | "" | Text direction: auto / ltr / rtl. | | require-interaction | boolean | false | Keep the notification visible until the user dismisses it. | | silent | boolean | false | Suppress sound/vibration. | | renotify | boolean | false | Re-alert when replacing a same-tag notification. | | manual | boolean | false | Mute the reactive notice path (the notify command still works). |

notice is a reactive input (no attribute): writing a changed value shows a notification. Per-call notify(title, options) options win per-key over these attribute defaults.

Observable Properties (outputs)

| Property | Event | Description | | ------------- | ------------------------------ | ----------------------------------------------------------------- | | permission | wcs-notify:permission-change | "prompt" / "granted" / "denied" / "unsupported", live. | | granted / denied / prompt / unsupported | wcs-notify:permission-change | Convenience booleans derived from permission. | | error | wcs-notify:error | { error, message } on a failure (never-throw), else null. | | errorInfo | wcs-notify:error-info-changed | Serializable failure taxonomy (WcsIoErrorInfo: stable code / phase / recoverable) on a failure, else null. Additive — derived from error, whose shape is unchanged. | | clicked | wcs-notify:click | { tag, data, action } of the last click (event-token source). | | closed | wcs-notify:close | { tag, data, action } of the last close. | | shown | wcs-notify:show | { tag, data, action } of the last shown notification. |

Commands

| Command | Description | | ------------ | --------------------------------------------------------------------------------- | | request() | Notification.requestPermission(); resolves to the normalized permission state. | | notify(title, options?) | Show a notification; returns its identifying tag. | | close(tag) | Dismiss the notification(s) with tag. | | closeAll() | Dismiss every notification this element has shown. |

CSS styling with :state()

<wcs-notify> reflects five boolean output states onto its ElementInternals CustomStateSet, so you can style it directly from CSS with the :state() pseudo-class — no data-wcs binding or extra class toggling required.

| State | On when | |-------|---------| | granted | wcs-notify:permission-change fires with "granted" | | denied | wcs-notify:permission-change fires with "denied" | | prompt | wcs-notify:permission-change fires with "prompt" | | unsupported | wcs-notify:permission-change fires with "unsupported" | | error | wcs-notify:error fires with a non-null detail (cleared on null) |

granted / denied / prompt / unsupported are a mutually exclusive group: each permission-change event sets exactly one of the four and clears the other three in the same pass.

wcs-notify:state(denied) ~ .fallback { display: block; }
wcs-notify:state(unsupported) ~ .fallback { display: block; }

form:has(wcs-notify:state(error)) .banner { display: block; }

Unlike attributes or classes, :state() cannot be written from outside the element, so there is no risk of confusing this output state with an input.

Browser support (:state(x) syntax): Chrome/Edge 125+, Safari 17.4+, Firefox 126+. In older browsers the states are simply never set — :state() selectors never match, but <wcs-notify> itself keeps working normally (graceful degradation, never-throw).

SSR: :state() cannot be serialized into HTML, so server-rendered markup never carries these states on first paint (@wcstack/server is unaffected). If you need to style the pre-hydration gap, pair your rule with wcs-notify:not(:defined) instead.

Debugging

Custom states are invisible in DevTools' Elements panel and attachInternals() cannot be called twice, so there is no console way to inspect them directly. Two debug-only aids are provided for that:

  • el.debugStates — a snapshot array of the currently-on state names (e.g. ["granted"]). It is not part of wc-bindable (not a bind target) and its shape is not a guaranteed contract — use it for debugging only.

  • The debug-states attribute (opt-in, default off) mirrors state changes onto data-wcs-state-granted / data-wcs-state-denied / data-wcs-state-prompt / data-wcs-state-unsupported / data-wcs-state-error attributes on the element, so the Elements panel highlights them as they toggle:

    <wcs-notify debug-states></wcs-notify>

Write your CSS against :state(), not data-wcs-state-*. The mirrored attributes exist purely to make state changes visible while debugging with DevTools open; they are not a supported styling hook.

Notes & limitations

  • Notifications outlive the page. Disconnecting <wcs-notify> (or calling dispose() on the Core) detaches its subscriptions but does not close open notifications — a notification is meant to persist past the page. Use close / closeAll to dismiss.
  • mode is fixed at connect time. The backend is chosen once, when the element connects (observe(mode)); there is no observedAttributes entry for mode. Changing the mode attribute on an already-connected element has no effect until you re-connect it (remove and re-insert).
  • The SW backend does not relay close. The @wcstack/notification/sw helper wires notificationclick only, not notificationclose. So under the sw backend closed / wcs-notify:close never fires (there is no cross-worker relay for close). clicked still works via the click relay; closed is a constructor-backend-only signal.
  • Push API is out of scope. This package wraps the Notifications API (local notifications). Server-initiated Push is a separate concern.
  • Silent failure handling (zero-log). Consistent with wcstack's zero-dependency philosophy, <wcs-notify> never logs or throws. A missing API → permission = "unsupported"; a not-granted permission or a show failure → the error property. Bind error / permission to react.
  • errorInfo taxonomy (additive). Alongside error, <wcs-notify> publishes a serializable errorInfo (wcs-notify:error-info-changed) that classifies the same failure into a stable WcsIoErrorInfo (code / phase / recoverable), without changing the error shape. The Core's own stable error code maps straight through: unsupportedcapability-missing (phase probe); a not-granted permission → not-allowed and a non-string title → invalid-argument (both phase start); a show failure → show-failed and a missing Service Worker backend → no-service-worker (both phase execute); any unexpected code folds defensively to notify-error (phase execute). All six are recoverable: false. errorInfo transitions exactly when error does (cleared to null on recovery); the shared WcsIoErrorInfo type and the WCS_NOTIFY_ERROR_CODE constants are exported.
  • SSR (@wcstack/server). Declares static hasConnectedCallbackPromise = true and exposes connectedCallbackPromise, so the server renderer waits for the connect-time permission probe before snapshotting.

Configuration

<wcs-notify> ships a click autoTrigger that is enabled by default. When enabled, the first <wcs-notify> to connect installs a single document-level click listener. A click on any element carrying the trigger attribute (data-notifytarget="<id>") calls notify() on the <wcs-notify> with that id:

<wcs-notify id="app-notify"></wcs-notify>
<button data-notifytarget="app-notify" data-notifybody="You have 1 new message">
  Notify me
</button>
  • The title is data-notifytitle if present, otherwise the trigger element's trimmed textContent; the body is the optional data-notifybody.
  • Because it is a document-wide click listener installed by default, disable it if you do not use the shortcut. Configuration is applied through bootstrapNotification(userConfig?) (which registers the element and applies the config in one call); getConfig() reads back the effective, deep-frozen config:
import { bootstrapNotification, getConfig } from "@wcstack/notification";

// Register <wcs-notify> and opt out of the document click listener:
bootstrapNotification({ autoTrigger: false });
// Or rename the trigger attribute:
bootstrapNotification({ triggerAttribute: "data-notify" });

getConfig(); // read the (deep-frozen) effective config

| Config key | Type | Default | Description | | ------------------ | --------- | ------------------- | ----------------------------------------------------------------- | | autoTrigger | boolean | true | Install the document-level click listener for data-notifytarget. | | triggerAttribute | string | data-notifytarget | Attribute a clickable element uses to target a <wcs-notify> by id. | | tagNames.notify | string | wcs-notify | Custom-element tag name to register. |

Call setConfig() / bootstrapNotification() before the elements connect for the change to take effect.

Headless usage (NotificationCore)

The Core has no DOM dependency and can be used directly with bind() from @wc-bindable/core:

import { NotificationCore } from "@wcstack/notification";

const notify = new NotificationCore();
await notify.observe();          // start watching permission + click relays
await notify.request();          // ask the user

notify.addEventListener("wcs-notify:click", (e) => {
  console.log((e as CustomEvent).detail); // { tag, data, action }
});

const tag = notify.notify("Hello", { body: "world", data: { room: 1 } });
// later:
notify.close(tag);
notify.dispose();                // detach subscriptions (open notifications stay)

The structural Core surface is normative across wcstack IO nodes (async-io-node-guidelines §3.9); to bind it into signals with no element at all, see @wcstack/signals — Binding a Core directly.

License

MIT