Skip to content
Scrim UI

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.

approvalhuman-in-the-loopagent

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
Auto-deny in 4:32

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-request

Follows 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-request

Component 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
approval-request.tsx
"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