better-push
Build notifications

UI components

Styled components you copy into your project and own.

@better-push/core/react ships headless components: semantic markup, no design system, style them however you like. That is the right library default and the wrong first impression.

So there is a second set: four styled components you copy into your project, in the shadcn model. They are built on the same hooks, they land as files in your repo, and the moment they are there they are yours to restyle, reorder, or delete half of.

npx @better-push/cli add --list
npx @better-push/cli add notification-bell

What ships

ComponentWhat it is
notification-inboxThe feed list: read state, infinite cursor, empty and error states, live updates
notification-bellAn unread badge over the inbox, in a popover
notification-preferencesGrouped per-type, per-channel switches, saved as one batch
push-toggleEnable browser push, with the denied and unsupported states handled

These previews render the same source files that the CLI and shadcn registry copy into your project. Use the controls to inspect the important states.

Notification inbox

Interactive notification inbox preview with populated, empty, loading, and error states.

Notification bell

Click the bell to open its inbox. Change the unread count with the control.

Interactive notification bell with an unread badge and inbox popover.

Notification preferences

The switches edit local state. Save and Discard become available after a change.

Interactive grouped notification preferences with in-app and push switches.

Push toggle

The status control includes idle, registering, subscribed, denied, unsupported, and error states. Preview actions never request browser permission.

Interactive browser push toggle covering available, busy, subscribed, denied, unsupported, and error states.

Each is Tailwind plus shadcn primitives, imports only from @better-push/core/react, and is covered by tests that render it against a mocked transport.

Two install paths

The CLI

npx @better-push/cli add notification-bell

Works offline, without shadcn, and without a docs domain being live. It finds your components directory from components.json when shadcn is set up and falls back to {src}/components/better-push/ otherwise, rewrites the import paths for your project's alias, refuses to overwrite without --force, and prints the shadcn primitives you still need.

Dependencies come with it: asking for the bell writes the inbox and the shared format helper too, because the bell imports them.

shadcn

export BETTER_PUSH_DOCS_ORIGIN="https://docs.example.com"
npx shadcn@latest add "$BETTER_PUSH_DOCS_ORIGIN/r/notification-inbox.json"

Same components, same source. If you are already in that workflow, use it.

One source, two builds

Both come from packages/ui-registry. A CI test asserts the docs-site registry and the CLI's embedded copy are byte-for-byte identical, because two consumers of one source is exactly the shape that drifts silently.

What you still need

The components use these shadcn primitives:

npx shadcn@latest add badge button popover scroll-area skeleton switch

add prints the ones your chosen components need, so you do not have to read this list.

Using them

app/layout.tsx
import { NotificationBell } from "@/components/better-push/notification-bell";

<header>
  <NotificationBell />
</header>
app/settings/notifications/page.tsx
import { NotificationPreferences } from "@/components/better-push/notification-preferences";
import { PushToggle } from "@/components/better-push/push-toggle";

export default function Page() {
  return (
    <>
      <PushToggle applicationServerKey={process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!} />
      <NotificationPreferences />
    </>
  );
}

The bell owns one feed hook and passes it to the inbox, so opening the popover does not start a second poll or a second SSE stream for the same user. If you render both separately, share the hook yourself:

const feed = useNotificationFeed();

<NotificationBell />
<NotificationInbox feed={feed} />

When to stay headless

If you already have a design system, the hooks are the API and these files are an example. useNotificationFeed, usePreferences and usePushRegistration are the whole surface; everything in these components is markup over them.

Copy one, read how it wires the hook, then write your own. That is a better outcome than fighting a className prop into matching your buttons.

On this page