1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Tab Title Badge

Tab Title Badge

ui

A tab-title badge: it puts "(3)" in front of the page title while the reader is in another tab, takes it back off the moment they return, and never steals the title from the router. Reach for it wherever something arrives while nobody is looking: unread messages in a chat, support inbox or team channel, a notification or alert count on a dashboard, an export, import, render or report that finishes minutes after it was kicked off, a CI run, deploy, migration or test suite a developer is waiting on, a background sync or upload queue, approvals or review requests piling up, a live auction or bid about to close, a pomodoro or cooking timer ending, the other player's turn, and a call or meeting waiting to be joined. Common asks it answers: "react document title unread count", "set document.title in react", "useDocumentTitle hook", "tab title notification badge", "show unread count in browser tab", "put (1) in the tab title react", "unread badge in page title", "blinking tab title react", "flash tab title on new message", "clear the tab badge when the user comes back", "detect when the user returns to the tab react", "visibilitychange react hook", "document.title resets on route change", "title flickers after navigation next.js", "document.title not updating in next app router", "restore the original document title on unmount", "document.title restored to the wrong page title", "two components fighting over document.title", "react-helmet just for a counter", "title badge shadcn", "notification count in tab title". shadcn/ui ships nothing that touches the tab: measured against the live catalogue on 2026-10-27 (65 items indexed, the 62 fetchable ones concatenated to 223,287 bytes of source), document.title, favicon, visibilitychange, Notification, rel="icon" and unread are all zero hits. It is the quiet counterpart to pulld install-prompt and notification-permission — it tells the reader something is waiting without asking the browser for permission to interrupt them, and unlike a toast it is still there when they come back in an hour. Written from the platform up, because the obvious version — read document.title on mount, prefix the count, put the remembered string back on unmount — is wrong in four separate ways that all look fine on one static page, and all four were measured in a real browser rather than remembered. One: the remembered title goes stale. In a single-page app the router writes a new title on every navigation, so the value captured at mount belongs to whatever page was open then, and restoring it puts a dead page's title over the live one; this hook adopts every title it did not write as the new base, so the badge always lands on the page the reader is actually on. Two: writing the same string still counts as a change — assigning document.title a value it already holds fires a mutation record all the same, so "re-apply the badge whenever the title changes" answers its own notification forever; in the probe it looped until a counter cut it off, and here one equality check ends it. Three: the change does not arrive where a first guess puts it. A plain assignment is a childList record on <title>, not characterData, and a title library that swaps the whole <title> element produces a childList record on <head> and leaves an observer bound to a node that is no longer in the document; one observer on <head> with subtree catches both, and neither narrower choice catches the other. Four: there is exactly one document.title, so a second instance is not a second badge but a second writer — the first to mount owns the title and the rest report isOwner false and hold still, the same shape pulld fullscreen-button and read-aloud use for the other page-wide APIs. The API is the count and what to do about it: count (0 and anything below 1 shows nothing), label for a word instead of a number ("New", "2 failed"), max to write anything larger as "99+", template to choose the join ("({badge}) {title}" by default, "{badge} • {title}" and "{title} — {badge} new" both work), showWhileSeen to keep it up in front of a reader who is already here, blinkMs to alternate with the page's own title, and onSeen to be told when they come back so your own counter can go to zero — the badge is a view of your state, never a copy of it. seenRule picks what "looking" means: the default follows Page Visibility, and "focus" additionally requires the window to be in front, which matters because a tab stays visible while its whole window sits behind the editor, so under the default rule a message that arrives while the reader is in another application clears itself unseen. useTabTitleBadge is the hook; <TabTitleBadge count={unread} /> is the same thing declaratively and renders nothing at all, because what it writes lives in the tab rather than on the page. Server-safe, no dependencies, not even an icon.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/tab-title-badge.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
components/ui/tab-title-badge.tsx