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-bellWhat ships
| Component | What it is |
|---|---|
notification-inbox | The feed list: read state, infinite cursor, empty and error states, live updates |
notification-bell | An unread badge over the inbox, in a popover |
notification-preferences | Grouped per-type, per-channel switches, saved as one batch |
push-toggle | Enable browser push, with the denied and unsupported states handled |
Component gallery
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
Notification bell
Click the bell to open its inbox. Change the unread count with the control.
Notification preferences
The switches edit local state. Save and Discard become available after a change.
Push toggle
The status control includes idle, registering, subscribed, denied, unsupported, and error states. Preview actions never request browser permission.
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-bellWorks 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 switchadd prints the ones your chosen components need, so you do not have to read
this list.
Using them
import { NotificationBell } from "@/components/better-push/notification-bell";
<header>
<NotificationBell />
</header>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.