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…

"use client"

import * as React from "react"

export interface UseDocumentTitleOptions {
  /** Restore `document.title` to the snapshot taken at mount when unmounting. Defaults to `true`. */
  restoreOnUnmount?: boolean
}

/**
 * Sets `document.title` to `title` and keeps it in sync on every change. At mount
 * the title as it stands **at that instant** is snapshotted into a ref; at unmount
 * it is restored from that snapshot when `restoreOnUnmount` (default `true`), and
 * left at the last title this hook wrote when `false`.

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/use-document-title.json

Prompt

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.title to in between.
  • Effect-only DOM access (SSR-safe) — every read/write of document.title happens inside useEffect, 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.
  • restoreOnUnmount is 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.

On This Page