# Approval Flow

> A React approval workflow: the sign-off chain of a request, showing who approved and who is next, with comments, reminders and history.

- Type: React component (Data Display)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add approval-flow`
- Docs: https://wingo-ui.com/components/approval-flow

## Interaction

Read the chain top to bottom: a green check per approved step, a ring with a clock on the step that waits, numbers on the ones after it, and every approver with a status ring, a decision badge, the time and their comment. When it is your turn a tinted bar asks for your decision: write an optional comment (type @ to mention a colleague, arrows and Enter pick one), then press Approve, Request changes or Reject. Reject opens a dialog (a bottom sheet on phones) that focuses the reason field and will not go on without a reason; Cmd or Ctrl + Enter there confirms, Escape cancels and returns focus. Approve never fires from the keyboard shortcut: decisions are explicit clicks. While a decision saves the pressed button spins and the others wait; an error from the server shows under the buttons. Once it saves, the bar folds away and keyboard focus moves to the flow instead of getting lost. Late approvers get a Send a reminder button, amber once the step is overdue. The last approval pops the final marker with a small bounce. The History row folds the timeline open and closed. In the compact variant the summary (stacked avatars with rings, 2 of 3 approvals, the status) is a button that opens the whole flow in a popover, a sheet on phones. Wider than 512px a horizontal flow lays its steps in a row; narrower it stacks. Everything is announced to screen readers and reduced motion turns the springs into instant changes.

## Installation

```bash
npx wingo-ui@latest add approval-flow
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/approval-flow` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/approval-flow.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Mention Input](https://wingo-ui.com/components/mention-input) ([Markdown](https://wingo-ui.com/components/mention-input.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { ApprovalFlow, type ApprovalStep } from "@/components/ui/approval-flow"

const initialSteps: ApprovalStep[] = [
  {
    id: "sales",
    name: "Sales manager",
    approvers: [{ id: "u-ryan", name: "Ryan Martin", role: "Sales", decision: "approved", decidedAt: "2026-09-22T14:05:00" }],
  },
  {
    id: "finance",
    name: "Finance director",
    description: "required over $10,000",
    dueAt: "2026-09-24T12:00:00",
    approvers: [{ id: "u-jessica", name: "Jessica Dunn", role: "Accounting" }],
  },
  { id: "ceo", name: "CEO", approvers: [{ id: "u-liam", name: "Liam Brooks", role: "Owner" }] },
]

export function DiscountApproval() {
  const [steps, setSteps] = useState(initialSteps)
  return (
    <ApprovalFlow
      title="15% discount · Keystone Construction"
      requester={{ name: "Michael Hayes" }}
      submittedAt="2026-09-22T11:20:00"
      amount={103400}
      currency="USD"
      currentUserId="u-jessica"
      steps={steps}
      labels={{ approve: "Approve", reject: "Decline", requestChanges: "Ask for changes" }}
      onDecision={async ({ stepId, decision, comment }) => {
        await fetch("/api/approvals", { method: "POST", body: JSON.stringify({ stepId, decision, comment }) })
        setSteps((prev) =>
          prev.map((step) =>
            step.id !== stepId
              ? step
              : {
                  ...step,
                  approvers: step.approvers.map((a) =>
                    a.id === "u-jessica" ? { ...a, decision, comment, decidedAt: new Date() } : a,
                  ),
                },
          ),
        )
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `steps` | `ApprovalStep[]` |  | The chain in order: { id, name, description?, rule?: "any" \| "all", approvers, dueAt?, skipped? }. Each approver is { id, name, avatar?, role?, decision?, decidedAt?, comment? (mention markup), delegatedFrom? }. The status of each step and of the request is derived from the decisions. |
| `title?` | `ReactNode` | `""` | The request, e.g. "15% discount · Keystone Construction". Without it the section is named by labels.title for screen readers. The demo shows a sample while the field is empty. |
| `requester?` | `{ name: string; avatar?: string }` |  | Who asked, shown with a small avatar in the meta line under the title. |
| `submittedAt?` | `TimeInput` |  | When it was submitted: a relative time in the meta line, the full date on hover. |
| `amount?` | `number` | `0` | The money the request is about, in major units, formatted with formatPrice in the meta line. 0 hides it (the demo then shows its sample amount). |
| `currency?` | `string` | `"USD"` | ISO 4217 code for the amount. |
| `status?` | `"draft" \| "pending" \| "approved" \| "rejected" \| "changes-requested" \| "cancelled" \| ""` | `""` | Overrides the status derived from the steps. Draft and cancelled only exist this way; any status other than pending hides the action bar and the active step ring. |
| `currentUserId?` | `string` | `""` | The person looking. When they are a pending approver of the active step the action bar shows and their row gets a You badge; anyone else sees who the request waits for. The demo's "View as" switch sets it while this field is empty. |
| `onDecision?` | `(input: ApprovalDecisionInput) => (void \| Promise<unknown>)` |  | Runs on Approve, Reject and Request changes with { stepId, decision: "approved" \| "rejected" \| "changes-requested", comment (markup), mentions (ids) }. A promise keeps the pressed button busy; a rejection shows its message under the buttons (or in the reject dialog). |
| `requireRejectComment?` | `boolean` | `true` | Reject opens a dialog (a sheet on phones) that needs a reason. false rejects at once with the comment as it is. |
| `allowRequestChanges?` | `boolean` | `true` | Shows the third action, Request changes, which sends the request back without rejecting it. |
| `onRemind?` | `(approver: ApprovalApprover, step: ApprovalStep) => (void \| Promise<unknown>)` |  | Adds a Send a reminder button to the pending approvers of the active step (amber once the step is overdue). A promise spins it; success turns it into Reminder sent and announces it. |
| `people?` | `MentionItem[]` |  | The colleagues @ can mention in the comment box (MentionInput). Without it the comment box is a plain Textarea. |
| `history?` | `ApprovalHistoryEntry[]` |  | The events: { id, at, actor: { name, avatar? }, type: submitted \| approved \| rejected \| changes-requested \| comment \| reminder \| resubmitted \| cancelled, comment?, stepId? }, shown as a feed timeline under a History (n) toggle. |
| `showHistory?` | `boolean` | `true` | Shows the History toggle when there is history. |
| `historyDefaultOpen?` | `boolean` | `false` | Starts with the history open. |
| `defaultOpen?` | `boolean` | `false` | Starts with the overlay open: the reject dialog in the full variant (when the viewer can decide), the details popover in compact. Control it with open and onOpenChange. |
| `open?` | `boolean` |  | Controlled overlay state (reject dialog or compact popover); pair it with onOpenChange. |
| `onOpenChange?` | `(open: boolean) => void` |  | Runs when the reject dialog or the compact popover opens or closes. |
| `orientation?` | `"vertical" \| "horizontal"` | `"vertical"` | horizontal lays the steps in a row with an avatar group under each, for page and invoice headers. Below 512px of width it stacks vertically anyway. |
| `variant?` | `"full" \| "compact"` | `"full"` | compact renders only the summary (stacked avatars with status rings, 2 of 3 approvals, the status badge) as a button that opens the full flow in a popover, a bottom sheet on phones. For table rows and cards. |
| `size?` | `"sm" \| "md"` | `"md"` | sm tightens the panel, the markers (24px), the avatars and the text for side panels. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The active step's ring, the your-turn bar and the Approve button. Decisions keep their status colors: green approved, red rejected, blue changes requested, amber waiting. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone for the active ring, the bar and the Approve button. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | Corner of the panel: 0, 12, 16 or 24px. |
| `surface?` | `boolean` | `true` | The raised panel look. false drops the background, shadow and padding for a flow inside a card of your own. |
| `maxAvatars?` | `number` | `4` | Compact: avatars before the +N chip. |
| `now?` | `Date \| number` |  | The moment relative times and overdue checks count from. Defaults to the live clock (useNow); demos pin 24.09.2026 16:40. |
| `locale?` | `string` | `""` | Locale for money, dates and relative times. Empty uses the UIProvider locale, then the browser's. |
| `labels?` | `ApprovalFlowLabelsInput (a deep Partial<ApprovalFlowLabels>)` |  | Every string, English by default (DEFAULT_APPROVAL_FLOW_LABELS): title, statuses, decisions, stepStatuses, events, any, all, due(date), overdue(date), approve, reject, requestChanges, comment, cancel, rejectTitle, rejectDescription, rejectReason, rejectReasonPlaceholder, rejectReasonRequired, mentionPeople, mentionEmpty, remind, reminderSent, reminded(name), remindError, yourTurn, waitingFor(names), progress(approved, required), history(count, formatted), submitted, skipped, delegated(from), you, decided(decision), decisionError. |
| `classNames?` | `Partial<Record<"header" \| "title" \| "meta" \| "status" \| "steps" \| "step" \| "marker" \| "connector" \| "stepName" \| "rule" \| "due" \| "approver" \| "comment" \| "remind" \| "actions" \| "history" \| "summary", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root section (the summary button in compact), merged last. |

## Parts

### ApprovalSummary (component)

The compact form alone for table cells: avatars with status rings, the progress and the status badge. A button when it gets onClick, with an aria-label like "2 of 3 approvals, Pending, Waiting for Ioana Dumitrescu". Props: { steps; status?; size?; max?; showCount?; showStatus?; locale?; labels?; classNames?; onClick? }.

### getApprovalStatus (function)

The derived overall status: approved when every step is approved or skipped, the active step's rejected or changes-requested, otherwise pending; draft without steps. Pure, for servers and lists. Signature: (steps: ApprovalStep[]) => ApprovalStatus.

### getActiveStep (function)

The first step that is neither approved nor skipped. Signature: (steps: ApprovalStep[]) => ApprovalStep | null.

### getStepStatus (function)

One step's outcome: any needs one approval, all needs every approver; a step without approvers counts as skipped. Signature: (step: ApprovalStep) => "approved" | "rejected" | "changes-requested" | "pending" | "skipped".

### getApprovalProgress (function)

Approvals given and needed across the chain (the 2 of 3 in the summary). Signature: (steps: ApprovalStep[]) => { approved: number; required: number }.

### DEFAULT_APPROVAL_FLOW_LABELS (constant)

Every English string: title ("Approval"), the status, decision, step status and history event names, the Approve / Reject / Request changes buttons, the reject dialog copy, due and overdue dates and the summary. Spread it into labels to translate a few. Type: ApprovalFlowLabels.

### ApprovalStep (type)

One stage of the chain: { id, name, description?, rule?: "any" | "all", approvers, dueAt?, skipped? }. any needs one approval, all needs every approver.

### ApprovalApprover (type)

One approver: { id, name, avatar?, role?, decision?, decidedAt?, comment?, delegatedFrom? }. comment takes mention markup such as @[Liam Brooks](u-liam).

### ApprovalDecisionInput (type)

What onDecision receives: { stepId, decision, comment, mentions }, with the ids of the people mentioned in the comment.

## Examples

- **Approved**: Every step green; replay the last approval to see the final marker pop.
- **Rejected with a reason**: The finance director rejected: the reason sits in a bubble under her name.
- **Horizontal, invoice header**: Steps in a row with avatar groups, inside a supplier invoice header. Stacks below 512px.
- **Compact in a table**: Five expense requests; tap a summary for the whole flow (a sheet on phones).
- **Changes requested, resubmitted**: The history tells the story; a colleague can nudge the late approver.
- **Phone, your turn**: Stacked full-width actions at 390px; Reject opens a bottom sheet on a phone. Brand tone.

## See also

- [Steps](https://wingo-ui.com/components/steps) ([Markdown](https://wingo-ui.com/components/steps.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Comment Thread](https://wingo-ui.com/components/comment-thread) ([Markdown](https://wingo-ui.com/components/comment-thread.md))
- [Audit Log](https://wingo-ui.com/components/audit-log) ([Markdown](https://wingo-ui.com/components/audit-log.md))
- [Mention Input](https://wingo-ui.com/components/mention-input) ([Markdown](https://wingo-ui.com/components/mention-input.md))

---

Canonical: https://wingo-ui.com/components/approval-flow
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
