The Bellnote widget is the bell-and-unread-badge pattern your users already know, without the weight. The entire integration is one script tag.
The snippet
<script src="https://cdn.bellnote.dev/widget.js" data-project="PROJ_ID" async></script>
Replace PROJ_ID with your project ID (shown on the publish-success screen and in project settings). Paste the tag before </body> on every page where the bell should appear. No npm package, no build step, no framework plugin.
What your users get
- A launcher button (bell icon) pinned to the corner you choose — fixed position, no layout shift.
- An unread badge counting entries published since the visitor last opened the panel.
- A panel with your latest entries on first click — the panel chunk lazy-loads only when needed, cached by the CDN.
- Subscribe by email inline in the panel (double opt-in), plus emoji reactions per entry.
Performance and privacy
- Core loader is under 5 KB gzipped — enforced in CI, the build fails if it grows.
- First-paint network cost is the loader plus one config request, in parallel.
- No cookies, no fingerprinting. Read state lives in
localStorageunderbellnote:<projectId>:lastSeen. Privacy is a feature. - Zero render-blocking: the script is
asyncand everything else loads lazily.
Theming (every plan, including Free)
Set these in Widget customization in the portal; they're served to the widget via the config endpoint:
| Setting | What it controls |
|---|---|
primaryColor |
Launcher and accent color |
textColor |
Panel text |
background |
Panel background |
position |
bottom-right or bottom-left |
launcherLabel |
Accessible label, e.g. "Updates" |
customCss |
Allowlisted properties only: color, background, font, border-radius, size tokens. url(), expression(), and @import are stripped. |
Free plans show a small "Powered by Bellnote" link; paid plans can turn it off.
SPA and framework notes
The widget is framework-agnostic vanilla JS. It survives client-side route changes, cleans up if the script is removed, and exposes a single global, window.Bellnote, with open() and close(). It works identically on React, Vue, Svelte, Rails, or plain HTML.
Accessibility
The launcher is a real <button> with an aria-label. The panel is focus-trapped, Esc closes it, and animations respect prefers-reduced-motion.