# Destructive Actions

Match the safeguard on an action to how much it destroys and whether you can take it back.

## Overview

Deleting a webhook, revoking an API key, and removing a team member all destroy something. Too few safeguards lose data you meant to keep. Too many train you to confirm without reading, so the one warning that matters gets skipped.

## Components

| Component                   | Role                                                                             |
| --------------------------- | -------------------------------------------------------------------------------- |
| `Button` with `negative`    | The action that destroys data or removes access, and its confirm button.         |
| `AlertDialog`               | Asks for a decision before the action runs. It never closes on an outside press. |
| `Field` and `Input`         | Collects the typed confirmation for an irreversible action.                      |
| `Toast` with an `action`    | Offers Undo after an action that ran at once.                                    |
| `Menu.Item` with `negative` | Places a destructive action at the end of a menu.                                |

## Structure

1. A trigger that names the action, such as Delete webhook, placed after the safe actions or at the end of a `Menu`.
2. A safeguard chosen by risk: Undo, a confirmation, or a typed confirmation.
3. A confirmation that names the object, states the consequence, and repeats the verb on its button.
4. A result: the item disappears, a `Toast` confirms it, and focus lands somewhere sensible.

```tsx
import { AlertDialog, Button, style } from '@tempoxyz/ds'

export function Demo() {
  return (
    <AlertDialog.Root>
      <AlertDialog.Trigger render={<Button variant="negative" />}>
        Delete webhook
      </AlertDialog.Trigger>
      <AlertDialog.Popup>
        <AlertDialog.Title>Delete this webhook?</AlertDialog.Title>
        <AlertDialog.Description>
          Tempo stops sending payment events to this endpoint. You can add it again later.
        </AlertDialog.Description>
        {/* Cancel comes first, so it takes focus on open and Enter by habit keeps the webhook. */}
        <div {...styles.actions()}>
          <AlertDialog.Close render={<Button variant="secondary" />}>Cancel</AlertDialog.Close>
          <AlertDialog.Close render={<Button variant="negative" />}>
            Delete webhook
          </AlertDialog.Close>
        </div>
      </AlertDialog.Popup>
    </AlertDialog.Root>
  )
}

namespace styles {
  // Below 640px the actions stack at full width with the primary action on top.
  export const actions = style({
    display: 'flex',
    flexDirection: 'column-reverse',
    gap: '2',
    marginTop: '8',
    selectors: { '& > *': { width: '100% !custom' } },
    '@media (min-width: 40rem)': {
      flexDirection: 'row',
      justifyContent: 'flex-end',
      selectors: { '& > *': { width: 'auto !custom' } },
    },
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/DeleteWebhook.tsx)

This reference confirmation is built from `AlertDialog` and `Button`. Cancel comes first in the markup, so it takes focus when the dialog opens.

## Guidelines

### Match the Safeguard to the Risk

| Risk                           | Examples                                  | Safeguard                                                                     |
| ------------------------------ | ----------------------------------------- | ----------------------------------------------------------------------------- |
| Easy to reverse                | Remove a contact, archive a notification  | Act at once, and offer Undo in a `Toast`.                                     |
| Recoverable with effort        | Delete a webhook, remove a team member    | Confirm in an `AlertDialog`.                                                  |
| Irreversible and wide-reaching | Revoke an API key, delete an organization | Confirm in an `AlertDialog`, and type the object's name.                      |
| Moves funds                    | Send a payment, swap tokens               | The review step is the confirmation. See [Payments](/docs/patterns/payments). |

Don't add an `AlertDialog` after a payment review. You already confirmed on the review screen, and a second prompt teaches you to dismiss prompts.

### Name the Action and Its Consequence

* Write the `AlertDialog.Title` as a question with the verb and the object, such as Revoke the Production key?
* State the consequence in `AlertDialog.Description`, including whether you can undo it.
* Repeat the verb on the confirm button, such as Revoke key, and use the `negative` variant.
* Put Cancel before the confirm button as `secondary`, following the order on the [Button](/docs/components/button) page.

**Do:** Name the object and the consequence, and repeat the verb on the button.

**Don't:** Ask a bare Are you sure? with Yes and No buttons.

### Start Focus on the Safe Choice

`AlertDialog.Popup` focuses its first tabbable element when it opens. When that's the destructive button, pass a ref to the Cancel button as `initialFocus`, so pressing Enter by habit keeps your data.

When the item disappears after the action, its trigger goes too. Pass `finalFocus` so focus moves to the list or its heading instead of the page body.

### Type to Confirm Irreversible Actions

```tsx
import { AlertDialog, Button, Field, Input, Rows, style, Surface, Tag, Toast } from '@tempoxyz/ds'
import { useState } from 'react'

export const fill = true
const key = 'Production'

function KeyRow() {
  const toast = Toast.useToast()
  const [invalid, setInvalid] = useState(false)
  const [open, setOpen] = useState(false)
  const [pending, setPending] = useState(false)
  const [revoked, setRevoked] = useState(false)
  const [typed, setTyped] = useState('')

  return (
    <Rows.Root dividers="none">
      <Rows.Row spacing="none">
        <Rows.Info>
          <Rows.Title>{key}</Rows.Title>
          <Rows.Description>Last used 2 minutes ago</Rows.Description>
        </Rows.Info>
        {revoked ? (
          <Tag>Revoked</Tag>
        ) : (
          <AlertDialog.Root
            onOpenChange={(next) => {
              setOpen(next)
              setInvalid(false)
              setTyped('')
            }}
            open={open}
          >
            <AlertDialog.Trigger render={<Button size="small" variant="negative" />}>
              Revoke key
            </AlertDialog.Trigger>
            <AlertDialog.Popup>
              <form
                noValidate
                onSubmit={(event) => {
                  event.preventDefault()
                  if (typed !== key) return setInvalid(true)

                  setPending(true)
                  setTimeout(() => {
                    setPending(false)
                    setOpen(false)
                    setRevoked(true)
                    toast.add({ intent: 'positive', title: `${key} key revoked` })
                  }, 1000)
                }}
              >
                <AlertDialog.Title>Revoke the {key} key?</AlertDialog.Title>
                <AlertDialog.Description>
                  Apps that use this key stop working immediately. You can't undo this.
                </AlertDialog.Description>
                <Field.Root disabled={pending} invalid={invalid} {...styles.field()}>
                  <Field.Label>Type {key} to confirm</Field.Label>
                  <Input.Group>
                    <Input.Control
                      autoComplete="off"
                      onValueChange={(value) => {
                        setTyped(value)
                        if (invalid && value === key) setInvalid(false)
                      }}
                      spellCheck={false}
                      value={typed}
                    />
                  </Input.Group>
                  <Field.Error match={invalid}>Type {key} exactly as shown.</Field.Error>
                </Field.Root>
                <div {...styles.actions()}>
                  <AlertDialog.Close disabled={pending} render={<Button variant="secondary" />}>
                    Cancel
                  </AlertDialog.Close>
                  <Button loading={pending} type="submit" variant="negative">
                    Revoke key
                  </Button>
                </div>
              </form>
            </AlertDialog.Popup>
          </AlertDialog.Root>
        )}
      </Rows.Row>
    </Rows.Root>
  )
}

export function Demo() {
  return (
    <div {...styles.page()}>
      <Toast.Provider>
        <Surface {...styles.card()}>
          <Surface.Header>
            <Surface.Title>API keys</Surface.Title>
          </Surface.Header>
          <KeyRow />
        </Surface>
        <Toast.Viewport />
      </Toast.Provider>
    </div>
  )
}

namespace styles {
  // Below 640px the actions stack at full width with the primary action on top.
  export const actions = style({
    display: 'flex',
    flexDirection: 'column-reverse',
    gap: '2',
    marginTop: '8',
    selectors: { '& > *': { width: '100% !custom' } },
    '@media (min-width: 40rem)': {
      flexDirection: 'row',
      justifyContent: 'flex-end',
      selectors: { '& > *': { width: 'auto !custom' } },
    },
  })

  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '26rem !custom',
    width: '100% !custom',
  })

  export const field = style({ marginTop: '6' })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    justifyItems: 'center',
    padding: '4',
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/RevokeKey.tsx)

Open Revoke key, type Production, and press Revoke key. Any other text shows an error instead.

* Ask for the object's own name, such as the key name, not a fixed word such as DELETE. Typing the name makes you read which key you're revoking.
* Keep the confirm button enabled, and show a `Field.Error` when the text doesn't match, as the Button page recommends over disabled buttons.
* Reserve typed confirmation for actions that can't be undone and affect other people or systems. Used everywhere, it becomes a chore you do without reading.

**Do:** Ask you to type the name of the object you're about to destroy.

**Don't:** Ask for typed confirmation on actions you can undo, such as removing a contact.

### Offer Undo When You Can

```tsx
import { Button, Identicon, Rows, style, Surface, Toast } from '@tempoxyz/ds'
import { useState } from 'react'

export const fill = true
type Contact = { address: string; name: string }

const initial: readonly Contact[] = [
  { address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F', name: 'Ada' },
  { address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', name: 'Payroll' },
  { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', name: 'Treasury' },
]

function Contacts() {
  const toast = Toast.useToast()
  const [contacts, setContacts] = useState(initial)

  return (
    <Surface {...styles.card()}>
      <Surface.Header>
        <Surface.Title>Contacts</Surface.Title>
      </Surface.Header>
      <Rows.Root>
        {contacts.map((contact) => (
          <Rows.Row key={contact.address}>
            <Identicon address={contact.address} aria-hidden size="24" />
            <Rows.Info>
              <Rows.Title>{contact.name}</Rows.Title>
              <Rows.Description>
                {contact.address.slice(0, 6)}…{contact.address.slice(-4)}
              </Rows.Description>
            </Rows.Info>
            <Button
              aria-label={`Remove ${contact.name}`}
              onClick={() => {
                // Removing a contact is easy to reverse, so it happens at once with an Undo.
                const index = contacts.indexOf(contact)
                setContacts((current) => current.filter((item) => item !== contact))
                toast.add({
                  action: {
                    label: 'Undo',
                    onClick: () =>
                      setContacts((current) => [
                        ...current.slice(0, index),
                        contact,
                        ...current.slice(index),
                      ]),
                  },
                  title: `${contact.name} removed`,
                })
              }}
              size="small"
              variant="text"
            >
              Remove
            </Button>
          </Rows.Row>
        ))}
      </Rows.Root>
    </Surface>
  )
}

export function Demo() {
  return (
    <div {...styles.page()}>
      <Toast.Provider>
        <Contacts />
        <Toast.Viewport />
      </Toast.Provider>
    </div>
  )
}

namespace styles {
  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '26rem !custom',
    width: '100% !custom',
  })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    justifyItems: 'center',
    padding: '4',
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/RemoveContact.tsx)

Remove a contact, then press Undo in the toast to restore it in place.

* When an action is easy to reverse, run it at once and show a `Toast` with an Undo `action`. Skip the confirmation.
* Keep another way back, such as adding the contact again. A toast dismisses itself, so it can't be the only path.
* Hovering the toast or moving focus into it pauses the dismiss timer, and F6 moves focus to the toasts.

### Save Negative Styling for the Decision

**Do:** Use `text` buttons for a removal you can undo, and keep `negative` for the confirm step.

**Don't:** Fill a list with `negative` buttons, which hides the one that matters.

In a `Menu`, give destructive items `intent="negative"` and place them last, after a `Menu.Separator`.

## States

| State    | What you show                                                                                                        |
| -------- | -------------------------------------------------------------------------------------------------------------------- |
| Default  | The trigger after the safe actions, or at the end of a `Menu`.                                                       |
| Loading  | `loading` on the confirm button, Cancel disabled, and the dialog open until the request finishes.                    |
| Error    | Keep the dialog open and explain the failure in it, such as Couldn't revoke key. The key is still active.            |
| Empty    | When you delete the last item, the list shows its first use [empty state](/docs/patterns/empty-states).              |
| Disabled | When you lack permission, say who can act, such as Only owners can revoke keys, instead of a silent disabled button. |
| Success  | Close the dialog, remove the item, and confirm with a `Toast`, such as Production key revoked.                       |

## Accessibility

* `AlertDialog` renders `role="alertdialog"`, traps focus, locks page scroll, and closes on Escape or a `Close` part. Focus returns to the trigger on close.
* `AlertDialog.Title` and `AlertDialog.Description` name and describe the popup, so always include both.
* The `negative` color repeats what the verb says. Never rely on red alone to mark a destructive action.
* The typed confirmation needs a visible `Field.Label` that contains the exact text to type.
* `Toast.Viewport` is a live region, so an Undo toast is announced. Its timers pause on hover, on focus, and while the window is blurred.
* Give each repeated trigger a specific name, such as Remove Ada, so the list of buttons reads clearly.

## Related

* [AlertDialog](/docs/components/alert-dialog) for confirmations
* [Button](/docs/components/button) for the `negative` variant and action order
* [Toast](/docs/components/toast) for Undo
* [Menu](/docs/components/menu) for destructive items in a menu
* [Forms](/docs/patterns/forms) for typed input
* [Payments](/docs/patterns/payments) for reviewing transfers
