# ApprovalCard

> Human-in-the-loop approval: what will run, how risky it is, and deny with a reason.

Source: https://agent-ui-kit-demo.vercel.app/docs/components/approval-card

## Installation

From npm:

```bash
npm i @dgesteves/agent-ui-kit ai
```

```tsx
import { ApprovalCard, ToolApprovalCard } from '@dgesteves/agent-ui-kit';
import '@dgesteves/agent-ui-kit/styles.css'; // once per app, or @import '@dgesteves/agent-ui-kit/tailwind.css' with Tailwind v4
```

Or as source, with the shadcn CLI:

```bash
npx shadcn@latest add @agent-ui-kit/approval-card
```

```tsx
import { ApprovalCard, ToolApprovalCard } from '@/components/agent-ui/approval-card';
```

## Usage

```tsx
// Bound to an AI SDK tool part in the approval flow:
<ToolApprovalCard part={part} onRespond={addToolApprovalResponse} risk="high" autoFocus />

// Or on its own:
<ApprovalCard
  toolName="run_command"
  input={{ command: 'pnpm add @upstash/ratelimit' }}
  risk="high"
  onApprove={() => approve()}
  onDeny={(reason) => deny(reason)}
/>
```

## API reference

### ApprovalCard

Human-in-the-loop approval for a pending agent action. Shows what will run,
how risky it is, and lets the user approve or deny by mouse or keyboard.
Critical actions require a second confirming press.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `toolName` (required) | `string` |  | Name of the tool or action that needs approval. |
| `title` | `ReactNode` |  | Heading. Defaults to the humanized tool name. |
| `description` | `ReactNode` |  | Why the agent wants to do this, e.g. the SDK's `approval.requestReason`. |
| `input` | `unknown` |  | Arguments the action will run with. |
| `risk` | `RiskLevel` ('low' \| 'medium' \| 'high' \| 'critical') | `'medium'` |  |
| `preview` | `ReactNode` |  | Custom arguments preview. Defaults to a command line for `{ command }` inputs, else JSON. |
| `status` | `ApprovalStatus` ('denied' \| 'pending' \| 'approved') | `'pending'` | Approve and deny fire once: further presses (a double click, Y then N) are ignored until `status` changes, the promise the handler returned settles, or the handler throws. |
| `reason` | `string` |  | Reason recorded with the decision, shown once resolved. |
| `automatic` | `boolean` | `false` | A policy, not a person, made the decision (the SDK's `approval.isAutomatic`): once resolved, the card reads "Auto-approved" or "Blocked by policy" instead of "Approved" or "Denied". |
| `onApprove` | `() => void \| PromiseLike<void>` |  | Fires once per decision: see `status`. May return a promise. |
| `onDeny` | `(reason?: string) => void \| PromiseLike<void>` |  | Fires once per decision: see `status`. May return a promise. |
| `shortcuts` | `boolean` | `true` | Y / N shortcuts while focus is inside the card. |
| `globalShortcut` | `boolean` | `false` | Also approve with ⌘/Ctrl+Enter from anywhere on the page while pending. |
| `autoFocus` | `boolean` | `false` | Move focus to the card when it mounts in the pending state. |
| `allowReason` | `boolean` | `true` | Offer an optional free-text reason when denying. |
| `approveLabel` | `string` | `'Approve'` |  |
| `denyLabel` | `string` | `'Deny'` |  |
| `headingLevel` | `HeadingLevel` | `3` | Heading level for the title, to fit your document outline. |

Other props go to the root `<section>`: `className`, `id`, `aria-*`, `data-*` and event handlers.

### ToolApprovalCard

`ApprovalCard` bound to an AI SDK tool part in the approval flow. Renders nothing for parts without
one, or while a policy's automatic decision is still arriving: nobody needs to act on it.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `part` (required) | `ToolPart` |  |  |
| `onRespond` (required) | `(response: ToolApprovalResponse) => void \| PromiseLike<void>` |  | Matches `useChat().addToolApprovalResponse`, so you can pass it directly. |
| `meta` | `ToolMeta` |  |  |

Also takes every `ApprovalCardProps` prop, except `toolName`, `input`, `status`, `onApprove`, `onDeny`, `reason`, `automatic`.

### ToolApprovalResponse

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` (required) | `string` |  |  |
| `approved` (required) | `boolean` |  |  |
| `reason` | `string` |  |  |

### RiskLevel

```ts
type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
```

### ApprovalStatus

```ts
type ApprovalStatus = 'denied' | 'pending' | 'approved';
```

## Accessibility

| Keys | Action |
| --- | --- |
| Y | Approve |
| N | Deny |
| ⌘/Ctrl ↵ | Approve (page-wide with `globalShortcut`) |
| Esc | Close the reason field and return to the card |

- A `<section>` named by its title and described by its description and a visually hidden line that spells out the shortcuts.
- Y and N only work while focus is inside the card, with no modifier and not while typing, which keeps them within WCAG 2.1.4. The buttons carry `aria-keyshortcuts`.
- `critical` actions need a second press within four seconds, and say so: "Critical action. Press approve again to confirm."
- Deciding announces "Approved" or "Denied" and moves focus to the card, so it isn’t lost when the buttons go away. `autoFocus` puts focus on a card that arrives pending.
- Each pending approval sends one decision: a double click, or Y then N, is ignored until `status` changes or the handler’s promise settles.
- The risk level is spelled out, not shown by color alone. `headingLevel` fits the title into your outline.

## Theming

Slots (`data-slot`): `approval-card`, `approval-deny`, `approval-approve`.

- `data-status` on `approval-card`: 'denied' | 'pending' | 'approved'
- `data-risk` on `approval-card`: 'low' | 'medium' | 'high' | 'critical'

Tokens it uses: `--aui-accent`, `--aui-accent-fg`, `--aui-bg`, `--aui-border`, `--aui-border-strong`, `--aui-fg`, `--aui-fg-muted`, `--aui-fg-subtle`, `--aui-font-mono`, `--aui-font-sans`, `--aui-hot`, `--aui-hot-fg`, `--aui-on-accent`, `--aui-on-hot`, `--aui-radius`, `--aui-ring`, `--aui-surface`, `--aui-surface-2`, `--aui-warn`, `--aui-warn-fg`. See https://agent-ui-kit-demo.vercel.app/docs/getting-started#theming.
