useAttention

Pro

Something waits on a person, or finished while they were away: a count, a "(n) " badge on the document title, and, only where the app and the browser both allow it, a system notification or a sound. Nothing here is on by itself. The badge is removed and the count cleared the moment the window gets focus or becomes visible again, or when the app calls clear(). Safe to import on the server: the browser is touched only in effects and handlers.

const attention = useAttention({ notify: settings.notify, sound: { src: "/ping.mp3", enabled: settings.sound } }) attention.signal({ kind: "approval", title: "Approve: raise the export batch size" })

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/use-attention

Usage

TSX · The example above
"use client"

import * as React from "react"
import { useAttention } from "@/lib/use-attention"
import { Button } from "@/components/ui/button"

/*
 * Notifications and sound stay off until the person turns them on here; the hook never asks for permission itself,
 * so the button does, from the click. The title badge clears as soon as this window gets focus again.
 */
export default function Example() {
  const [notify, setNotify] = React.useState(false)
  const attention = useAttention({ notify })
  const canNotify = attention.permission === "granted"
  return (
    <div className="flex flex-col items-start gap-3 text-sm">
      <p>
        Waiting on you: <strong>{attention.count}</strong>. Switch to another tab, then look at this one’s title.
      </p>
      <div className="flex flex-wrap gap-2">
        <Button size="sm" variant="outline" onClick={() => attention.signal({ kind: "approval", title: "Approve: raise the export batch size", body: "Relay is waiting on you." })}>
          An approval arrives
        </Button>
        <Button size="sm" variant="outline" onClick={() => attention.signal({ kind: "finished", title: "Nightly import finished", body: "1,204 rows, no errors." })}>
          Work finishes
        </Button>
        <Button size="sm" variant="ghost" onClick={attention.clear}>
          Clear
        </Button>
      </div>
      <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
        {attention.permission === "unsupported" && <span>This browser has no system notifications.</span>}
        {attention.permission === "default" && (
          <Button size="xs" variant="outline" onClick={() => attention.requestPermission()}>
            Allow system notifications
          </Button>
        )}
        {attention.permission === "denied" && <span>System notifications are blocked in this browser’s settings.</span>}
        {canNotify && (
          <label className="inline-flex items-center gap-1.5">
            <input type="checkbox" checked={notify} onChange={(e) => setNotify(e.target.checked)} />
            Send a system notification for each one
          </label>
        )}
      </div>
    </div>
  )
}

Functions

useAttention({ title, badge = true, notify = false, sound }: AttentionOptions = {})

Something waits on a person, or finished while they were away: a count, a "(n) " badge on the document title, and, only where the app and the browser both allow it, a system notification or a sound. Nothing here is on by itself. The badge is removed and the count cleared the moment the window gets focus or becomes visible again, or when the app calls clear(). Safe to import on the server: the browser is touched only in effects and handlers.

const attention = useAttention({ notify: settings.notify, sound: { src: "/ping.mp3", enabled: settings.sound } }) attention.signal({ kind: "approval", title: "Approve: raise the export batch size" })

Types

AttentionKind
What is asking for the person: a step waiting on them, or work that finished or failed while they were away.
"approval" | "finished" | "failed"
AttentionSignal
One thing to tell the person about. `title` and `body` are shown as given, as text, in the system notification.
{ kind: AttentionKind title: string body?: string | undefined }
AttentionPermission
`Notification.permission`, or "unsupported" where the browser has no Notification API (and on the server).
NotificationPermission | "unsupported"
AttentionOptions
What the app allows. Everything is off unless named here, and what the browser hasn't granted stays off whatever the app says.
{ /** The page's own title, so "(2) " is put in front of it and it comes back exactly when the count clears. Left out, the title found when the first signal lands is kept and restored; pass it when the app sets titles itself, so a route change isn't undone. */ title?: string | undefined /** Put "(n) " in front of the document title while anything waits. On unless the app turns it off; the count is
Attention
What useAttention returns: the count, and the three things the app can do about it.
{ /** Signals since the person was last here. 0 when nothing waits. */ count: number /** Record something that needs the person: adds one to the count, badges the title, and notifies or plays a sound when those are allowed. */ signal: (what: AttentionSignal) => void /** Back to 0: the title is restored and notifications the hook sent are closed. Also happens on its own when the window gets focus o