Click, confirm with the recipient count, send, then a status line. Can be blocked with a reason.
Build this with your agent
Copy a ready prompt for Claude Code, Cursor or any coding agent.
"use client"
import * as React from "react"Use it for any action that messages other people and cannot be taken back. It is never one click to send: the button becomes "Send to 3 people?" with Send and Cancel, and only then does your handler run. The count is part of the question, so nobody sends to a list they have not seen.
It is usually the action of a status-strip.
Installation#
pnpm dlx shadcn@latest add https://realgood.site/r/confirm-send.json
Or, with the @crisp namespace set up:
pnpm dlx shadcn@latest add @crisp/confirm-send
This also adds the shadcn button and lucide-react.
Usage#
import { ConfirmSend } from "@/components/ui/confirm-send"const [sending, setSending] = React.useState(false)
const [sentCount, setSentCount] = React.useState<number | null>(null)
<ConfirmSend
count={saved.length}
label="Email this summary"
sending={sending}
sentCount={sentCount}
blockedReason={dirty ? "Save your changes first" : undefined}
onSend={async () => {
setSending(true)
try {
setSentCount(await api.sendSummary())
} finally {
setSending(false)
}
}}
/>The component owns only the "are you sure" step. You own sending and sentCount.
Notes#
- Count the saved list. Pass the number of people the send will actually reach. If the list on screen has unsaved edits, use
blockedReasonto stop the send until they are saved. A confirmation that is open when the list changes is dismissed, because what it says no longer matches what would be sent. - Blocked and empty.
blockedReasondisables the button and sets it as the button's tooltip and its accessible description. Whencountis 0 the same happens withemptyReason.blockedReasonwins when both apply. - "Sent to N". Show it by passing
sentCount. It appears beside the button, which becomes an outline button, and it hides while the button is blocked. Reset it tonullwhen the list changes or you want it gone. The component never clears it. - Button text. With a count it reads "
labelto 3 people" and with none just "label". Change the nouns withnounandnounPlural. - Sending state. The confirmation closes as soon as Send is pressed, then
sendingdisables the button. The label does not change whilesendingis true, so pair it with your own indicator if the send is slow. - Fragment output. It renders a fragment of buttons and text, so put it in a flex container that supplies the gap.
What it does not do#
- It does not send anything. Your
onSenddoes. The component owns only the "are you sure" step. - It does not know the list. Pass the number of people the send will actually reach. If the screen has unsaved edits, block the send with
blockedReason. - It does not retry, queue or track delivery. You own
sendingandsentCount, and the message for a failure. - It is not access control. A disabled button stops no one. Your server decides who may send.
- It makes no compliance claim. Confirming with a count is a UI cue, not compliance.
Props#
ConfirmSend#
The prop types are exported as ConfirmSendProps. It renders no wrapper element, so it takes no className or ref.
| Prop | Type | Description |
|---|---|---|
count | number | How many recipients the saved list reaches. Required. |
onSend | () => void | Promise<void> | Called after the person confirms. Required. |
noun | string | One recipient. Default "person". |
nounPlural | string | Plural of noun. Default "people" for "person", otherwise noun plus "s". |
blockedReason | string | Blocks the send and explains why. Cancels an open confirmation. |
emptyReason | string | The reason shown when count is 0. Default "Add at least one recipient first". |
sending | boolean | Disables the button while your send runs. Default false. |
sentCount | number | null | Shows "Sent to N" when set. Default null. |
label | string | Idle button label. Default "Send now". |
Agent prompt#
Paste this into your coding agent (Claude Code, Cursor, Codex or similar) in your project. Replace the bracketed parts with your own.
Goal: put a confirm-then-send action on my [screen] for [what is sent, e.g. the weekly summary], so nothing goes out on one click and the person sees how many people it reaches first.
Install: npx shadcn@latest add https://realgood.site/r/confirm-send.json
Read the installed files (components/ui/confirm-send.tsx) before writing any code. Do not guess props.
Contract:
<ConfirmSend count={number} onSend={() => void | Promise<void>} noun?="person" nounPlural?="people" blockedReason?={string} emptyReason?={string} sending?={boolean} sentCount?={number | null} label?="Send now" />
It renders a fragment of buttons and text and takes no className, so put it in a flex container with a gap.
Wiring rule: the component owns only the "are you sure" step. I own `sending` and `sentCount`. `count` is the SAVED list the send will reach, not what is on screen. If the list has unsaved edits, pass blockedReason ("Save your changes first"). onSend calls my own API, sets `sending` in a try/finally, and on success sets `sentCount` to the number my API says it reached. Reset sentCount to null when the list changes. Do not send from the browser straight to a mail service.
States to handle: idle, confirming ("Send to 3 people?"), sending, sent ("Sent to 3"), failed (my own error text, and the button is usable again), blocked (reason shown as tooltip and description), empty list (count 0).
Acceptance checks (run them and show me the output):
1. Typecheck and lint pass.
2. One click never calls onSend. Send in the confirmation calls it once.
3. With unsaved edits the button is disabled and says why.
4. A failed send shows an error, does not show "Sent to N", and can be retried.
5. Keyboard only: Tab reaches Send and Cancel, and the blocked reason is read out.
UI only: it does not send, store, queue or check permissions, and it makes no compliance claim. No new dependencies or abstractions beyond this. If something is unclear, ask me.Examples by sector#
Example data only, to show the wording. These are not claims about any real organisation, and the component proves nothing about a sector's rules. The pattern is the same in each; only the data changes.
- Education. "Send to 12 staff?" before a reminder goes to everyone who has not finished training.
- Manufacturing. "Send to 4 owners?" before a recall notice goes to the owners of overdue gauges.
- Engineering. "Send to 5 people?" before a change order's release notice goes to those who build from the drawing.
- Health. "Send to 3 staff?" before a licence-expiry reminder goes out. The count is of people, and the message carries no patient details.
Next#
Comes after: data-table, approval-step, save-bar. Leads to: audit-timeline.