Browse all components
Approval Request UI
The human-in-the-loop confirmation — what the agent wants to do, the exact details, and allow or deny before it acts.
Preview
Run database migration
Deploy Agent is requesting approval
Runs an irreversible migration on the production database. 4 tables, ~2 min estimated downtime.
npm run migrate -- --env=production --confirm
Presets
Props
Also takes onAllow, onDeny.
Agent promptClaude Code · Cursor · any agent
Add the Approval Request UI component from the Scrim UI registry to this project and use it in the configuration described below.
Approval Request UI — The human-in-the-loop confirmation — what the agent wants to do, the exact details, and allow or deny before it acts.
## 1. Install
```bash
npx shadcn@latest add @scrimui/approval-request
```
This writes a single file to `components/ui/approval-request.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/approval-request to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { ApprovalRequest } from "@/components/ui/approval-request";
<ApprovalRequest
title="Run database migration"
requester="Deploy Agent"
description="Runs an irreversible migration on the production database. 4 tables, ~2 min estimated downtime."
detail="npm run migrate -- --env=production --confirm"
status="pending"
onAllow={() => {}}
onDeny={() => {}}
/>
```
## 3. The configuration that matters
This is the component's "Pending" state — Context, the exact action, and an auto-deny countdown so requests never linger.
Every prop is at its default value. Keep the call site minimal — do not write out default values.
## 4. Wire the handlers
`onAllow`, `onDeny` are stubs in the snippet above (`() => {}`). Connect them to this project's own state and data layer instead of shipping the empty functions.
Reference: https://scrimui.dev/components/approval-requestFollows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.
Install
npx shadcn@latest add @scrimui/approval-requestComponent source
React + Tailwind component using shadcn/ui (button, card). The install command adds these dependencies automatically.
When copying source manually, install the required primitives first:
npx shadcn@latest add button card"use client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type ApprovalState = "pending" | "approved" | "denied";
export type ApprovalRequestProps = {
title: string;
requester?: string;
description?: string;
detail?: string;
status?: ApprovalState;
onAllow?: () => void;
onDeny?: () => void;
className?: string;
};
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
function ShieldIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="16" height="16" {...props}>
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
<path d="m9 12 2 2 4-4" />
</svg>
);
}
function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="12" height="12" {...props}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
function XIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="12" height="12" {...props}>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* ApprovalRequest */
/* ------------------------------------------------------------------ */
export function ApprovalRequest({
title,
requester,
description,
detail,
status = "pending",
onAllow,
onDeny,
className = "",
}: ApprovalRequestProps) {
return (
<Card className={`gap-0 py-0 rounded-xl border border-border bg-card p-4 ${className}`}>
<div className="flex items-start gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-amber-100 text-amber-600 dark:bg-amber-900/40 dark:text-amber-400">
<ShieldIcon />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground">{title}</p>
{requester && (
<p className="mt-0.5 text-xs text-muted-foreground">{requester} is requesting approval</p>
)}
{description && (
<p className="mt-1.5 text-sm leading-5 text-muted-foreground">
{description}
</p>
)}
{detail && (
<pre className="mt-2 overflow-x-auto rounded-lg bg-muted p-2.5 font-mono text-xs leading-5 text-foreground">
{detail}
</pre>
)}
{status === "pending" ? (
<div className="mt-3 flex items-center gap-2">
<Button variant="default" size="sm"
type="button"
onClick={onAllow}
className="inline-flex h-8 items-center gap-1.5 rounded-md bg-primary px-3.5 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90"
>
<CheckIcon />
Allow
</Button>
<Button variant="outline" size="sm"
type="button"
onClick={onDeny}
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-3.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted"
>
<XIcon />
Deny
</Button>
<span className="ml-auto text-xs text-muted-foreground">Auto-deny in 4:32</span>
</div>
) : (
<p
className={`mt-3 inline-flex items-center gap-1.5 text-xs font-medium ${
status === "approved"
? /* -700 in light: emerald-600 on white is 3.65:1 at 12px. */
"text-emerald-700 dark:text-emerald-400"
: "text-red-600 dark:text-red-400"
}`}
>
{status === "approved" ? <CheckIcon /> : <XIcon />}
{status === "approved" ? "Approved — action executed" : "Denied — action blocked"}
</p>
)}
</div>
</div>
</Card>
);
}
When to use it
- Ask for approval only for irreversible, costly or sensitive actions — not every step.
- Always show the concrete action (command, diff, recipients), not a vague 'proceed?'
- Include an auto-deny timeout for unattended requests.
- Say what happened after the decision — approved actions should confirm execution.
- Make Allow/Deny keyboard accessible and order them consistently.
What breaks in production
- Asking for approval with no context — users can't consent to what they can't see.
- No timeout — pending requests pile up and the user returns to a stale queue.
- Hiding the cost or blast radius of the action (recipients, rows affected, money).
- Treating approval as a formality the agent can bypass with retries.
Guides
Related Components
Status for an AI agent working on its own — running, waiting on you, completed or failed, with what it did last.
The approval card as a lifecycle: a decision that survives a closed tab, is idempotent across two open ones, and says what happened when it lands after the request expired.
The checklist an agent writes for itself and then edits mid-run — steps added, skipped and re-ordered, without the list jumping under the reader.
One agent passing work to another — who asked, what was carried across, and what the receiving agent was not told.