Archive or Delete

Puts Archive and Delete side by side with what happens to each thing under both, so people don't delete what they only meant to put away.

StopsDeleted forever instead of archived

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/archive-or-delete.json

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

Usage

archive-or-delete-usage.tsx
import { ArchiveOrDelete, type RemovalChoice, type RemovalItem } from "@/components/ui/archive-or-delete"

<ArchiveOrDelete
  key={run}
  name="#launch-2025"
  items={ITEMS}
  archiveSummary="Reversible. Unarchive anytime from Settings."
  deleteSummary="Permanent. Nobody can undo this, including support."
  onConfirm={(choice) => new Promise<void>((resolve) => setTimeout(() => {
    setDone(choice)
    resolve()
  }, 600))}
  onCancel={() => setRun((n) => n + 1)}
/>

What it catches

  • Choosing permanent deletion when archiving would do
  • Hidden differences between the two choices, by listing only the items whose fate differs first
  • Ambiguous confirm buttons, by naming the choice and the thing ("Delete forever")

Props

ArchiveOrDelete props
PropTypeDefault
name* (required)

Name of the thing being removed, e.g. "#launch-2025".

string—
items* (required)

Everything affected, each with its fate under both choices.

RemovalItem[]—
onConfirm* (required)

Runs with the chosen option. Return a promise to show a pending state; a rejection is caught.

(choice: RemovalChoice) => void | Promise<void>—
onCancel

Called when the user cancels.

() => void—
value

Controlled choice. Defaults to "archive", the reversible one.

RemovalChoice—
defaultValue

Initially selected choice when uncontrolled. Defaults to archive.

RemovalChoice"archive"
onValueChange

Called when the selected choice changes.

(choice: RemovalChoice) => void—
autoFocus

Focus the reversible choice on mount, so Enter archives rather than deletes.

booleantrue
title

Dialog title; receives the thing's name.

(name: string) => React.ReactNode(n) => `Remove ${n}?`
archiveLabel

Label of the Archive option.

string"Archive"
archiveSummary

One line on how to undo an archive, e.g. "Unarchive anytime from Settings".

string"Reversible. Unarchive anytime."
deleteLabel

Label of the Delete option.

string"Delete"
deleteSummary

One honest line on recovery, e.g. "Can't be undone" or "Recoverable for 30 days".

string"Permanent. This can't be undone."
confirmArchiveLabel

Confirm button text when Archive is chosen.

(name: string) => string(n) => `Archive ${n}`
confirmDeleteLabel

Confirm button text when Delete is chosen.

(name: string) => string(n) => `Delete ${n} forever`
pendingLabel

Confirm button text while onConfirm is running.

string"Working…"
cancelLabel

Cancel button text.

string"Cancel"
sameLabel

Label above the items both choices treat the same way.

string"Same either way"

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

Accessibility

  • Choices are a role="radiogroup" labelled by the title, each option described by its summary
  • Archive is focused on open (autoFocus, default true)
  • The confirm button sets aria-busy while pending
  • The Delete highlight animation is skipped with prefers-reduced-motion; fates are written in text, not just colour

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

Limitations

This is a UX guard rail. Enforce on the server: Authorise the delete on the server and, where possible, keep a recovery window; the dialog does not make deletion reversible.

  • Only describes the fates you pass in; it does not know what your backend really deletes
  • It does not archive or delete anything; onConfirm does

What it stops

Deleted forever instead of archived

Also known as

archive · delete forever · soft delete · inbox · project archive · hide · remove

Dependencies

Pairs well with