Hooks
useDocumentTitle
A hook that syncs document.title to a value for as long as a component is mounted, restoring the pre-mount title on unmount.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/use-document-title.jsonPrompt
Build a React + TypeScript "useDocumentTitle" hook (no dependencies beyond
React; uses `document.title` only).
Contract
- `useDocumentTitle(title: string, options?: { restoreOnUnmount?: boolean }): void`.
- `restoreOnUnmount` defaults to `true`.
- Returns nothing — the hook's only output is the side effect of writing
`document.title`.
Behavior
- On mount, snapshot the *current* `document.title` into a ref before the
hook ever writes to it. The snapshot is taken exactly once per mount, not
re-taken when `title` or `restoreOnUnmount` change later.
- On every render where `title` changed (including the first), set
`document.title = title`.
- On unmount, if `restoreOnUnmount` is true at that moment, write
`document.title` back to the mount-time snapshot; if false, leave whatever
title is currently set untouched.
- `restoreOnUnmount` is read live at the moment of unmount (via a ref kept in
sync by its own effect), not captured once at mount — flipping the option
right before a component unmounts changes whether the restore fires.
- All `document` access happens inside effects, never during render, so the
hook does nothing on the server and only touches the DOM after the client
mounts — safe to call from a server-rendered component.
Multi-instance behavior (state this explicitly, don't paper over it)
- Multiple components can call this hook independently and simultaneously
(e.g. a layout-level instance plus a page-level instance). Each instance
only knows the title it saw at its own mount time — there is no shared
registry between instances.
- Whichever instance mounts (or re-renders with a new `title`) last wins the
visible tab title, because it writes `document.title` last.
- If instances unmount in a different order than they mounted (not strict
last-in-first-out — e.g. the outer/earlier instance unmounts while an
inner/later instance is still mounted), the earlier instance's restore
overwrites the still-mounted instance's title with an older snapshot — a
visible "jump back" past the title the still-mounted instance actually set.
This is a known, accepted limitation of independent per-instance snapshots,
not something this hook tries to silently fix.
Rendering & styling
- The hook renders nothing and owns no DOM node — consumers own all UI. Any
visible indicator that mirrors the title (e.g. an in-page "simulated tab"
preview in a demo) is the consumer's own markup, styled with semantic
tokens (`bg-muted`, `text-muted-foreground`, `text-foreground`); the hook
itself has no styling surface.
Customization levers
- `restoreOnUnmount` — set to `false` for a title change that should "stick"
after the component is gone (e.g. leaving a page but wanting the browser
history entry to keep reflecting the title it had).
- A prefix/suffix template helper (e.g. always rendering
`` `${prefix} ${title} ${suffix}` ``, like a fixed brand suffix) is
intentionally NOT built in — the hook takes the final string as-is so
callers stay in full control of format; layer a small formatting function
on top if a consistent template is needed app-wide.
- A flashing/alternating title (swapping between the real title and
"New message!" every second or two to grab attention on an inactive tab)
is intentionally NOT built in either — it needs its own interval and a
policy for when to stop (on tab focus, after N flashes), which is a
distinct concern from "set one title and restore it." Layer a `setInterval`
on top that calls this hook with an alternating `title` value, and clear it
on the `visibilitychange` event when the tab regains focus.Concepts
- Mount-time snapshot, not a live capture — the "previous title" is read once, right when the hook mounts, into a ref; it is never re-read later, so it always reflects what the page looked like before this instance took over, not whatever some other code sets
document.titleto in between. - Effect-only DOM access (SSR-safe) — every read/write of
document.titlehappens insideuseEffect, never during render, so the hook has nothing to guard for on the server and behaves identically whether the component was server-rendered or not. restoreOnUnmountis read live, at unmount time — it's tracked through a ref kept current by its own effect, so a consumer that flips the option right before removing the component gets the up-to-date decision, not one frozen at mount.- Multi-instance ordering is unmanaged, on purpose — this hook has no shared state across instances. Later mount (or later title update) always wins the visible title; but unmounting out of strict last-in-first-out order can make an earlier instance's restore overwrite a still-mounted instance's title, jumping the tab back further than expected. Fixing that for real would mean introducing a shared title stack that every instance pushes/pops from — a deliberately bigger hook than this one, and out of scope here (see Customization levers for where to extend instead of patching around it).
- Formatting and attention-grabbing are layered on top, not built in — prefix/suffix templates and flashing "new activity" titles are common asks but orthogonal to "set and restore one title," so they're left for the caller to compose around this hook rather than becoming hidden options here.