Approval Request

The agent stops and asks first, showing the exact thing it wants to do.

PROGet Approval Request and 168 other Pro components with pro lifetime — Get Pro

Category
AI Agent
Access
Pro
Source
676 lines · one file
States
10 drawn

The decision inside it

Rejecting is as useful as approving

A bare "No" leaves the agent stuck. Reject offers reasons in one click and hands them to the agent, so it can try another way instead of asking again.

When to use it

Use Approval Request when an agent needs a person before it acts: what it wants to do, the risk, a preview of the change, remember-my-choice, a countdown and Reject with reasons. Approve and Reject may return a Promise; an error keeps the request open.

Installation

How you get it

Paste this into Claude Code, Cursor, Codex or any coding agent. It installs the file and uses it as it is, instead of writing a new one.

Use Liro's Approval Request for this.
Install it: npx shadcn@latest add @liro/approval-request
(the @liro registry is set up in components.json, with LIRO_KEY in .env.local)
Import it from @/components/ui/approval-request.
Do not rewrite or restyle it. Pass my data through its props
and keep its built-in states.

For the full brief with every prop and the mistakes assistants usually make, use Copy prompt above. Pro components install through the @liro registry with your CLI token: see the Namespaced tab.

Usage

components/approve-email.tsx
import { ApprovalRequest } from "@/components/ui/approval-request";

export function ApproveEmail({ approve, reject }: { approve: () => Promise<void>; reject: (reason?: string) => Promise<void> }) {
  return (
    <ApprovalRequest
      title="Send 2 payment reminders?"
      description="The agent wants to email the customers with overdue invoices."
      tool="email.send"
      risk="medium"
      details={[
        { label: "To", value: "billing@vertically.dev, ap@orbit.io" },
        { label: "Subject", value: "Your invoice is overdue" },
      ]}
      onApprove={approve}
      onReject={reject}
      reasons={["Wrong customers", "Not now"]}
    />
  );
}

API reference

“Copy prompt” above puts a full brief on your clipboard, install command, dependencies, props, and the mistakes an assistant usually makes with this component.

Props

Read from the component’s own TypeScript interface when the site is built, so this table cannot drift from the file you install.

TypeNotes
titleReact.ReactNode YesWhat the agent wants to do, as an action: "Send an email to Olivia".
descriptionReact.ReactNode NoWhy it wants to, in a sentence.
toolstring NoThe tool it will call: `email.send`.
toolIconReact.ReactNode NoThe service the tool belongs to, shown before its name: a brand mark.
riskApprovalRisk NoDefault `medium`.
detailsApprovalDetail[] NoThe facts of the action: To, Subject, Amount.
previewReact.ReactNode NoThe thing itself: the message, the change, the file.
changesApprovalChange[] NoBefore and after, for an action that edits a record.
scopesstring[] NoThe permissions the action uses: "gmail.send", "crm.write".
targetReact.ReactNode NoWhere it lands: "Vertically · Gmail". Shown beside the tool.
requestedByReact.ReactNode NoWho or what asked: "Renewal agent".
riskExplanationReact.ReactNode NoWhy it carries this risk, in a sentence from your backend.
requireConfirmationboolean NoApprove asks once more before it counts.
onRequestChanges(note: string) => void | Promise<void> NoAdds "Request changes": send it back with a note instead of a yes or no. May return a promise.
submittingboolean NoSet while your server is acting on the answer, if you track it yourself.
errorReact.ReactNode NoAn error from your side, shown above the buttons.
statusApprovalStatus NoControlled state. Leave it out and the card keeps its own.
defaultStatusApprovalStatus No–
onApprove(options: { remember: boolean }) => void | Promise<void> NoMay return a promise: the card waits for it, and stays open if it rejects.
onReject(reason?: string) => void | Promise<void> NoMay return a promise, like `onApprove`.
onEdit() => void NoAdds an Edit button, for changing the action before it runs.
rememberLabelReact.ReactNode NoAdds a checkbox with this label: "Always allow email.send in this run".
reasonsstring[] NoOne-click reasons offered on Reject.
expiresAtDate | number | string NoWhen the request stops being answerable. Shows a countdown.
onExpire() => void No–
decidedByReact.ReactNode NoShown on the folded record: "by you", "by Michael Carter". Default "by you".
decidedAtReact.ReactNode NoShown on the folded record: "10:42".

Dependencies

npm packages

clsxframer-motionlucide-reacttailwind-merge

Liro foundations, installed alongside it

liro-themebadgebuttoncheckboxtextarea

States

All of them are drawn by the component. None is left for you to design.

  • Waiting
  • Rejecting
  • Confirming
  • Submitting
  • Failed to submit
  • Approved
  • Rejected
  • Changes requested
  • Expired
  • High risk

Accessibility

  • Keyboard reachable throughout, in visual order
  • Focus trapped in overlays, Escape closes them
  • Focus returns to whatever opened it
  • Live regions announce what changed, not that something did
  • Honours prefers-reduced-motion

Responsive behavior

  • Sizes to its container, not the viewport
  • Usable from 320px up
  • Pointer-based interaction, so trackpad and touch both work
  • No hover-only affordances

Used in screensAll screens