Unsent Marker

Brings a typed-but-never-sent message back as a clear "Not sent" bubble, so people don't think they replied.

StopsThought I replied but never sent it

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/unsent-marker.json

The files land in your project. They're yours to edit.

Usage

unsent-marker-usage.tsx
import { readUnsent, saveUnsent, UnsentMarker, useUnsentDraft, type UnsentDraft } from "@/components/ui/unsent-marker"

<UnsentMarker
  text={draft.text}
  at={draft.at}
  recipient={CHATS[chat].name}
  onSend={async (text) => {
    await new Promise((resolve) => setTimeout(resolve, 400))
    send(text)
  }}
  onEdit={(text) => {
    setValue(text)
    clear()
  }}
  onDiscard={clear}
/>

What it catches

  • Text left in the composer and never sent
  • Who hasn't received it
  • Accidental discard (two taps)
  • Send failures, with an error

Props

UnsentMarker props
PropTypeDefault
text* (required)

The unsent text.

string—
at

When it was last typed (ms).

number—
variant

`bubble` sits in the thread where the reply would be; `row` is a one-line preview for conversation lists.

"bubble" | "row""bubble"
recipient

Who it was meant for, e.g. "Priya".

string—
onSend

Sends it now. Return a promise to show a pending state; a rejection keeps the marker with an error.

(text: string) => void | Promise<void>—
onEdit

Moves it back into the composer.

(text: string) => void—
onDiscard

Throws it away (asks once).

() => void—
notSentLabel

Label on the unsent bubble.

string"Not sent"
typedText

`{when}` is filled in.

string"Typed {when}."
recipientText

`{recipient}` is filled in.

string"{recipient} hasn't received it."
privateText

Used when there's no `recipient`.

string"Only you can see it."
sendLabel

Label of the Send button.

string"Send"
editLabel

Label of the Edit button.

string"Edit"
discardLabel

Label of the Discard button.

string"Discard"
confirmDiscardLabel

Label of the second Discard tap.

string"Discard draft?"
failedText

Shown when sending fails.

string"Couldn't send. Check your connection and try again."

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Bubble is a labelled group including the text
  • Send failures use role=alert
  • Buttons have text labels; icons are aria-hidden

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Drafts are kept where your useUnsentDraft storage keeps them (per device)
  • Doesn't know about sends from other devices unless you clear the draft

What it stops

Thought I replied but never sent it

Also known as

draft · unsent · chat · reply · message composer · whatsapp · inbox · conversation list

Field type

Dependencies

Pairs well with