Retract Window

Shows how long Edit and Unsend really stay open for a sent message, and who will keep the original anyway.

StopsTrusted an unsend that didn't work

Installation

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

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

Usage

retract-window-usage.tsx
import { RetractWindow } from "@/components/ui/retract-window"

<RetractWindow
  sentAt={SENT}
  now={NOW}
  timeZone="Asia/Kolkata"
  policy="imessage"
  editsUsed={1}
  recipients={[
    { id: "p", name: "Priya", read: true },
    { id: "r", name: "Rahul", canRetract: false },
    { id: "a", name: "Anu" },
  ]}
  onEdit={() => setLog("Editing…")}
  onUnsend={() => new Promise<void>((resolve) => setTimeout(resolve, 700))}
  onDeleteForMe={() => setLog("Deleted from this phone only.")}
  onSendCorrection={() => setLog("Writing a correction…")}
>
  Salary bands for the team are in the sheet, 18–24 LPA. Sharing with HR only.
</RetractWindow>

What it catches

  • Time left to edit and to unsend, per the platform's rules
  • Edits used against a limit (iMessage: 5)
  • Recipients who already read it or whose app keeps the original
  • "Delete for me" labelled as leaving everyone else's copy
  • Who still has it after an unsend, and a correction once windows close

Props

RetractWindow props
PropTypeDefault
children

The sent message, rendered as a bubble.

React.ReactNode—
sentAt* (required)

When the message was sent.

Date | string | number—
policy

A preset ("imessage", "whatsapp", "slack") or your own rules.

RetractPlatform | RetractPolicy"imessage"
recipients

Who it went to, whether they've read it and whether their app can remove it.

RetractRecipient[][]
editsUsed

Edits already made (iMessage allows five).

number0
now

Pass for a deterministic start; the countdown then runs from it. Otherwise the clock is read after mount.

Date—
timeZone

IANA zone for displayed times.

string—
locale

Locale for displayed times.

string"en-IN"
onEdit

Opens the editor. Without it there's no Edit row.

() => void—
onUnsend

Removes it for everyone. Return a promise for a pending state; a rejection means it stayed.

() => void | Promise<void>—
onDeleteForMe

Removes it only from this device.

() => void—
onSendCorrection

Offered once the windows have closed or an unsend fails.

() => void—
messages

Override any visible string.

Partial<RetractWindowMessages>—

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

Accessibility

  • Edit/Unsend buttons are described by their window text (aria-describedby)
  • Only a window closing is announced (role=status), never the countdown
  • Failures use role=alert; after unsend, focus moves to the result
  • The draining bar is aria-hidden; time left is also text

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

Limitations

  • Presets reflect the platforms' help pages as of the verified date and change without notice
  • Slack windows depend on each workspace's admin settings; pass the real limits
  • Does not edit or unsend; your callbacks do, and a failed unsend shows an error

Rules

Rules last verified:

Confirmed: iMessage unsend 2 min, edit up to 5 times within 15 min; WhatsApp edit 15 min, Delete for everyone within two days; Slack members can edit/delete any time by default, admins can limit. Check the platforms' current help pages. Copied components don't update themselves, so re-check after rule changes (see updating a component).

What it stops

Trusted an unsend that didn't work

Also known as

unsend · delete for everyone · edit message · whatsapp · imessage · slack · recall · chat

Field type

Dependencies

Pairs well with