Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Data Display

Approval Flow

Data Display

Approval Flow React component for Tailwind CSS v4

ToastAvatar
Live props
Content
title
amountNone
currency
State
status
currentUserId
historyDefaultOpen
defaultOpen
Behavior
requireRejectComment
allowRequestChanges
locale
Layout
showHistory
orientation
maxAvatars4
Appearance
variant
size
tone
color
radius
surface
Code
import { ApprovalFlow } from "@/components/ui/approval-flow"

<ApprovalFlow />
  1. Home
  2. Components
  3. Data Display
  4. Approval Flow
Pro

Approval Flow React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

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

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.

Approval Flow is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add approval-flow, together with Mention Input, Timeline, Avatar, Badge, Button and 11 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add approval-flow

Approval Flow is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import ApprovalFlow from @/components/ui/approval-flow.

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

  • 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.

  • titledefault ""
    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.

  • amountdefault 0
    number

    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).

  • currencydefault "USD"
    string

    ISO 4217 code for the amount.

  • statusdefault ""
    "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.

  • currentUserIddefault ""
    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).

  • requireRejectCommentdefault true
    boolean

    Reject opens a dialog (a sheet on phones) that needs a reason. false rejects at once with the comment as it is.

  • allowRequestChangesdefault true
    boolean

    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.

  • showHistorydefault true
    boolean

    Shows the History toggle when there is history.

  • historyDefaultOpendefault false
    boolean

    Starts with the history open.

  • defaultOpendefault false
    boolean

    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.

  • orientationdefault "vertical"
    "vertical" | "horizontal"

    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.

  • variantdefault "full"
    "full" | "compact"

    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.

  • sizedefault "md"
    "sm" | "md"

    sm tightens the panel, the markers (24px), the avatars and the text for side panels.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    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.

  • colordefault ""
    string

    Any CSS color; replaces the tone for the active ring, the bar and the Approve button.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg"

    Corner of the panel: 0, 12, 16 or 24px.

  • surfacedefault true
    boolean

    The raised panel look. false drops the background, shadow and padding for a flow inside a card of your own.

  • maxAvatarsdefault 4
    number

    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.

  • localedefault ""
    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.

Dependencies

motionlucide-reactmention-inputtimelineavatarbadgebuttondialogtextareatooltippopoverrelative-timecommerceuse-localeuse-controllable-statemotiontonesui-config

Files

  • components/ui/approval-flow.tsx

FAQ

Is the React Approval Flow component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Approval Flow?

Run npx wingo-ui@latest add approval-flow. It also adds the Wingo UI items it builds on (Mention Input, Timeline, Avatar, Badge, Button and 11 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/approval-flow.

How does Approval Flow behave on phones?

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.

See also

StepsTimelineComment ThreadAudit LogMention Input

More React data display components

AvatarCalendar ViewCode BlockContact cardDescription ListDiff Viewer
All 23 React data display components
PreviousToastNext Avatar
View as

15% discount · Keystone Construction · Quote #Q-2026-118

Michael Hayessubmitted 2 days ago
Pending$103,400
  1. Sales manager, Approved

    Michael Hayes is the requester, so a teammate approves

    • Ryan MartinSales · 2 days ago
      Approved
      The client signs for 3 years, the discount is justified.
  2. Finance director, In progress

    required over $10,000

    Overdue since 09/24/2026, 12:00 PM
    • Jessica DunnYouAccounting
      Waiting
  3. 3
    CEO, Upcoming

    only over $50,000

    • Liam BrooksOwner
      Waiting

Your approval is needed

currentUserId: "u-jessica" · requireRejectComment: true · locale: "en-US" · onDecision → (none yet)