# ApprovalCard

> A human-in-the-loop gate before a side effect: the agent states what it is about to do, and the footer swaps its approve and deny buttons for a one-line verdict once someone decides.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: AI Agents
- Tags: ai, agents, approval, human-in-the-loop, gate
- Docs: https://fancy-ui.rama.app/docs/components/approval-card
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/approval-card

## Import

```svelte
<script lang="ts">
  import { ApprovalCard } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { ApprovalCard } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` (required) | `string` | — | What permission is being asked for, e.g. "Run database migration" |
| `description` | `string` | — | Muted second line under the title — the consequence, the blast radius |
| `state` | `"pending" \| "approved" \| "denied"` | `"pending"` | Which side of the gate we are on (bindable). Buttons exist only while pending; writing a resolved value from outside settles the card without firing a callback |
| `destructive` | `boolean` | `false` | Marks the action as irreversible: red approve button, a warning tint on the card, and an alert mark on the shield so the warning is not carried by colour alone |
| `approveLabel` | `string` | `"Approve"` | Label for the approve button |
| `denyLabel` | `string` | `"Deny"` | Label for the deny button |
| `onApprove` | `() => void` | — | Called when approve is pressed, after state has been written |
| `onDeny` | `() => void` | — | Called when deny is pressed, after state has been written |
| `busy` | `boolean` | `false` | The consumer is executing the decision: both buttons go disabled and the card is marked aria-busy |
| `sound` | `boolean` | `false` | Plays select on both approve and deny, once the user has enabled sound |

## Snippets

| Snippet | Description |
| --- | --- |
| `children` | Detail region between the header and the footer — a diff, a command preview, the payload about to be posted |

## Examples

### Basic Usage

```svelte
<script lang="ts">
	import { ApprovalCard } from "fancy-ui-svelte";
	import type { ApprovalState } from "fancy-ui-svelte";

	// No timer, no loop: the demo only moves when someone actually decides
	// something, which is the whole point of the component.
	let branchState = $state<ApprovalState>("pending");
	let migrationState = $state<ApprovalState>("pending");

	const settled = $derived(
		[branchState, migrationState].filter((state) => state !== "pending").length
	);

	function reset() {
		branchState = "pending";
		migrationState = "pending";
	}
</script>

<div class="flex w-full max-w-xl flex-col gap-3 p-6">
	<ApprovalCard
		sound
		title="Create branch fix/retry-backoff"
		description="Branches from develop and pushes an empty commit so CI has something to run."
		bind:state={branchState}
	/>

	<ApprovalCard
		sound
		title="Run database migration"
		description="Adds two columns to invoices and backfills 41,880 rows. There is no down migration."
		destructive
		approveLabel="Run migration"
		denyLabel="Not now"
		bind:state={migrationState}
	>
		<pre
			class="ft-approval-preview text-foreground/80 overflow-x-auto rounded-md px-2.5 py-2 font-mono text-xs">pnpm db:migrate --env production --yes</pre>
	</ApprovalCard>

	<p class="text-muted-foreground mt-1 text-xs">
		{#if settled === 0}
			Both gates are waiting. Nothing runs until you press something.
		{:else}
			{settled} of 2 decided.
			<button
				type="button"
				class="text-foreground underline decoration-dotted underline-offset-2"
				onclick={reset}
			>
				Reset
			</button>
		{/if}
	</p>
</div>

<style>
	.ft-approval-preview {
		background: color-mix(in oklab, currentColor 7%, transparent);
	}
</style>
```
