Skip to content
AI Interfaces block

Bulk Approval Queue

Forty decisions and an honest way to clear them: bulk approve that cannot reach the high-impact rows, a select-all that says how many it takes, and the auto-approve deadline on every line.

307 lineslucide-reactAdded 27 Aug 2026Updated 10 Sept 2026
  • approval
  • queue
  • bulk
  • review
  • agent

What's included

  • components/approval-queue.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add approval-queue

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Waiting for you

6 actions the agent will not take on its own · 2 must be opened individually

Actions awaiting approval. High-impact actions have no bulk checkbox and must be opened individually.
Select the 4 actions that can be approved in bulkActionAmountDeadline

Refund order 41822

Damaged on arrival · photo attached · customer since 2023

£38.00Auto-approves in 3h

Refund order 41830

Never delivered · carrier confirmed loss

£12.50Auto-approves in 3h

Refund order 41833

Duplicate charge · matched to payment intent pi_3Qk

£64.00Auto-approves in 6h

Close account for Meridian Foods

Requested by an admin · 41 users · 3 years of data

Deletes data — opens individually

—Open Close account for Meridian Foods

Refund order 41840

Late delivery · goodwill, within policy

£9.99Auto-approves in 12h

Issue account credit to Halden Group

Outage compensation · agent proposed 3 months

Above the bulk limit — opens individually

£4,200.00Open Issue account credit to Halden Group

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/approval-queue.tsx
'use client'

/**
 * <ApprovalQueue> — a queue of decisions, and the honest way to bulk them.
 *
 * Human in the Loop already had the single approval card, the diff review,
 * the confidence recommendation and the scope dialog: four surfaces that
 * assume one decision at a time. That assumption survives a demo and dies
 * in week two, when the queue has forty items and the reviewer starts
 * hunting for a way to clear it.
 *
 * THE ONE THING THIS GETS RIGHT THAT MOST DO NOT
 *
 * Bulk approve exists — refusing to build it just moves the rubber-stamp
 * into a rage-click on every row — but it cannot reach the items that
 * matter. Anything the agent marked high-impact is checkbox-less by
 * design, with the reason in its place: "opens individually". So the fast
 * path stays fast for the eighteen refunds under a tenner, and the two
 * decisions that can lose money or delete data still cost a person a
 * click each.
 *
 * SELECT-ALL MEANS WHAT IS ON SCREEN
 *
 * A select-all that silently spans the filter is how a reviewer approves
 * things they never saw. The header checkbox says how many it will take,
 * and when a filter is on it says that too. Its indeterminate state is
 * set through a ref — the attribute does not exist in markup, and a
 * three-state box drawn as a styled span is invisible to a screen reader.
 *
 * TIME IS PART OF THE DECISION
 *
 * Real approval systems auto-approve on a timer, or the work stops. If
 * that is true, the row says when — "auto-approves in 3h" is the most
 * decision-changing text on this screen, and burying it in a policy doc
 * is how a reviewer discovers the rule by being surprised by it.
 *
 * ACCESSIBILITY: a real `<table>`, because this is tabular and reviewers
 * sort it. Every row checkbox is labelled with what it approves, so the
 * screen-reader run is "approve refund, order 41822" and not twenty
 * identical "select" boxes. The action bar is `aria-live="polite"` and
 * counts in words.
 */

import * as React from 'react'
import { AlertTriangle, ArrowRight, Check, Clock, ShieldAlert, X } from 'lucide-react'

export interface ApprovalItem {
  id: string
  /** What the agent wants to do, phrased as the action it will take. */
  action: string
  context: string
  amount?: string
  /** Hours until the policy approves it without a human. */
  autoApproveIn?: number
  /**
   * High-impact items are excluded from every bulk control. The string is
   * shown in place of the checkbox — the reason has to be readable, or the
   * missing box reads as a bug.
   */
  needsIndividualReview?: string
}

export interface ApprovalQueueProps {
  items?: ApprovalItem[]
  className?: string
}

const DEFAULT_ITEMS: ApprovalItem[] = [
  {
    id: '41822',
    action: 'Refund order 41822',
    context: 'Damaged on arrival · photo attached · customer since 2023',
    amount: '£38.00',
    autoApproveIn: 3,
  },
  {
    id: '41830',
    action: 'Refund order 41830',
    context: 'Never delivered · carrier confirmed loss',
    amount: '£12.50',
    autoApproveIn: 3,
  },
  {
    id: '41833',
    action: 'Refund order 41833',
    context: 'Duplicate charge · matched to payment intent pi_3Qk',
    amount: '£64.00',
    autoApproveIn: 6,
  },
  {
    id: 'acct-9921',
    action: 'Close account for Meridian Foods',
    context: 'Requested by an admin · 41 users · 3 years of data',
    needsIndividualReview: 'Deletes data — opens individually',
  },
  {
    id: '41840',
    action: 'Refund order 41840',
    context: 'Late delivery · goodwill, within policy',
    amount: '£9.99',
    autoApproveIn: 12,
  },
  {
    id: 'cr-4402',
    action: 'Issue account credit to Halden Group',
    context: 'Outage compensation · agent proposed 3 months',
    amount: '£4,200.00',
    needsIndividualReview: 'Above the bulk limit — opens individually',
  },
]

export function ApprovalQueue({ items = DEFAULT_ITEMS, className = '' }: ApprovalQueueProps) {
  const [selected, setSelected] = React.useState<string[]>([])
  const [resolved, setResolved] = React.useState<Record<string, 'approved' | 'declined'>>({})

  const pending = items.filter((i) => !resolved[i.id])
  /* The set a bulk control is allowed to touch, and nothing beyond it. */
  const bulkable = pending.filter((i) => !i.needsIndividualReview)
  const allSelected = bulkable.length > 0 && selected.length === bulkable.length
  const someSelected = selected.length > 0 && !allSelected

  const headerBox = React.useRef<HTMLInputElement>(null)
  React.useEffect(() => {
    /* Indeterminate is a property, not an attribute — it cannot be set in JSX. */
    if (headerBox.current) headerBox.current.indeterminate = someSelected
  }, [someSelected])

  const toggle = (id: string) =>
    setSelected((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]))

  const resolve = (ids: string[], outcome: 'approved' | 'declined') => {
    setResolved((r) => ({ ...r, ...Object.fromEntries(ids.map((id) => [id, outcome])) }))
    setSelected((s) => s.filter((id) => !ids.includes(id)))
  }

  const held = pending.filter((i) => i.needsIndividualReview).length

  return (
    <section className={`mx-auto w-full max-w-4xl px-4 py-16 sm:px-6 ${className}`}>
      <div className="overflow-hidden rounded-2xl border border-border bg-card">
        <header className="flex flex-wrap items-center gap-3 border-b border-border px-5 py-4">
          <div className="min-w-0 flex-1">
            <h2 className="text-base font-semibold text-foreground">Waiting for you</h2>
            <p className="mt-0.5 text-xs text-muted-foreground">
              {pending.length} {pending.length === 1 ? 'action' : 'actions'} the agent
              will not take on its own
              {held > 0 ? ` · ${held} must be opened individually` : ''}
            </p>
          </div>
        </header>

        {/*
          Live region and a count in words: the bar appears and changes
          under the reviewer's hands, and the number in it is the whole
          basis for pressing a button labelled "approve".
        */}
        {selected.length > 0 ? (
          <div
            aria-live="polite"
            className="flex flex-wrap items-center gap-2 border-b border-border bg-muted/50 px-5 py-3"
          >
            <p className="min-w-0 flex-1 text-sm text-foreground">
              {selected.length} selected — every one of them a refund under the
              bulk limit.
            </p>
            <button
              type="button"
              onClick={() => resolve(selected, 'declined')}
              className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-border bg-background px-3 text-xs font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
            >
              <X aria-hidden className="h-3.5 w-3.5" />
              Decline {selected.length}
            </button>
            <button
              type="button"
              onClick={() => resolve(selected, 'approved')}
              className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-primary px-3 text-xs font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
            >
              <Check aria-hidden className="h-3.5 w-3.5" />
              Approve {selected.length}
            </button>
          </div>
        ) : null}

        <div className="relative overflow-x-auto">
          <table className="w-full text-start text-sm">
            <caption className="sr-only">
              Actions awaiting approval. High-impact actions have no bulk
              checkbox and must be opened individually.
            </caption>
            <thead>
              <tr className="border-b border-border text-xs text-muted-foreground">
                <th scope="col" className="w-10 px-5 py-2">
                  <input
                    ref={headerBox}
                    type="checkbox"
                    checked={allSelected}
                    onChange={() => setSelected(allSelected ? [] : bulkable.map((i) => i.id))}
                    /*
                      accent-primary, not a hand-drawn box: the tokens here
                      are oklch(), so hsl(var(--primary)) is not a colour
                      and the check would render browser-blue.
                    */
                    className="h-4 w-4 rounded border-field accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                  />
                  <span className="sr-only">
                    Select the {bulkable.length} actions that can be approved in bulk
                  </span>
                </th>
                <th scope="col" className="px-2 py-2 font-medium">
                  Action
                </th>
                <th scope="col" className="px-2 py-2 text-end font-medium">
                  Amount
                </th>
                <th scope="col" className="px-5 py-2 text-end font-medium">
                  Deadline
                </th>
              </tr>
            </thead>
            <tbody className="divide-y divide-border">
              {items.map((item) => {
                const outcome = resolved[item.id]
                const boxId = `approve-${item.id}`
                return (
                  <tr key={item.id} className={outcome ? 'opacity-60' : undefined}>
                    <td className="px-5 py-3 align-top">
                      {item.needsIndividualReview || outcome ? (
                        <span
                          aria-hidden
                          className="flex h-4 w-4 items-center justify-center text-muted-foreground"
                        >
                          <ShieldAlert className="h-3.5 w-3.5" />
                        </span>
                      ) : (
                        <>
                          <label htmlFor={boxId} className="sr-only">
                            {item.action}
                          </label>
                          <input
                            id={boxId}
                            type="checkbox"
                            checked={selected.includes(item.id)}
                            onChange={() => toggle(item.id)}
                            className="h-4 w-4 rounded border-field accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                          />
                        </>
                      )}
                    </td>
                    <td className="px-2 py-3 align-top">
                      <p className="font-medium text-foreground">{item.action}</p>
                      <p className="mt-0.5 text-xs text-muted-foreground">{item.context}</p>
                      {item.needsIndividualReview && !outcome ? (
                        <p className="mt-1 inline-flex items-center gap-1.5 rounded-md bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-400">
                          <AlertTriangle aria-hidden className="h-3 w-3" />
                          {item.needsIndividualReview}
                        </p>
                      ) : null}
                      {outcome ? (
                        <p className="mt-1 text-xs font-medium text-muted-foreground">
                          {outcome === 'approved' ? 'Approved' : 'Declined'} just now ·{' '}
                          <button
                            type="button"
                            onClick={() =>
                              setResolved(({ [item.id]: _drop, ...rest }) => rest)
                            }
                            className="rounded underline underline-offset-2 transition hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                          >
                            Undo
                          </button>
                        </p>
                      ) : null}
                    </td>
                    <td className="px-2 py-3 text-end align-top tabular-nums text-foreground">
                      {item.amount ?? '—'}
                    </td>
                    <td className="px-5 py-3 text-end align-top">
                      {item.needsIndividualReview ? (
                        <a
                          href="#review"
                          className="inline-flex items-center gap-1 rounded text-xs font-medium text-primary underline-offset-4 transition hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                        >
                          Open
                          <ArrowRight aria-hidden className="h-3 w-3 rtl:rotate-180" />
                          <span className="sr-only"> {item.action}</span>
                        </a>
                      ) : item.autoApproveIn ? (
                        /* The rule people otherwise learn by being surprised. */
                        <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
                          <Clock aria-hidden className="h-3 w-3" />
                          Auto-approves in {item.autoApproveIn}h
                        </span>
                      ) : (
                        <span className="text-xs text-muted-foreground">No deadline</span>
                      )}
                    </td>
                  </tr>
                )
              })}
            </tbody>
          </table>
        </div>
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/approval-queue.tsx and import it where you need the section:

import { ApprovalQueue } from '@/components/approval-queue'

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
itemsApprovalItem[]DEFAULT_ITEMS, className = ''
classNamestring—

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

approval-queue.html
<!--
  Bulk Approval Queue — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section class="mx-auto w-full max-w-4xl px-4 py-16 sm:px-6 ">
  <div class="overflow-hidden rounded-2xl border border-border bg-card">
    <header class="flex flex-wrap items-center gap-3 border-b border-border px-5 py-4">
      <div class="min-w-0 flex-1">
        <h2 class="text-base font-semibold text-foreground">Waiting for you</h2>
        <p class="mt-0.5 text-xs text-muted-foreground">6 actions the agent will not take on its own · 2 must be opened individually</p>
      </div>
    </header>
    <div class="relative overflow-x-auto">
      <table class="w-full text-start text-sm">
        <caption class="sr-only">Actions awaiting approval. High-impact actions have no bulk checkbox and must be opened individually.</caption>
        <thead>
          <tr class="border-b border-border text-xs text-muted-foreground">
            <th scope="col" class="w-10 px-5 py-2">
              <input type="checkbox" class="h-4 w-4 rounded border-field accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" />
              <span class="sr-only">Select the 4 actions that can be approved in bulk</span>
            </th>
            <th scope="col" class="px-2 py-2 font-medium">Action</th>
            <th scope="col" class="px-2 py-2 text-end font-medium">Amount</th>
            <th scope="col" class="px-5 py-2 text-end font-medium">Deadline</th>
          </tr>
        </thead>
        <tbody class="divide-y divide-border">
          <tr>
            <td class="px-5 py-3 align-top">
              <label for="approve-41822" class="sr-only">Refund order 41822</label>
              <input id="approve-41822" type="checkbox" class="h-4 w-4 rounded border-field accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" />
            </td>
            <td class="px-2 py-3 align-top">
              <p class="font-medium text-foreground">Refund order 41822</p>
              <p class="mt-0.5 text-xs text-muted-foreground">Damaged on arrival · photo attached · customer since 2023</p>
            </td>
            <td class="px-2 py-3 text-end align-top tabular-nums text-foreground">£38.00</td>
            <td class="px-5 py-3 text-end align-top">
              <span class="inline-flex items-center gap-1 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>Auto-approves in 3h</span>
            </td>
          </tr>
          <tr>
            <td class="px-5 py-3 align-top">
              <label for="approve-41830" class="sr-only">Refund order 41830</label>
              <input id="approve-41830" type="checkbox" class="h-4 w-4 rounded border-field accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" />
            </td>
            <td class="px-2 py-3 align-top">
              <p class="font-medium text-foreground">Refund order 41830</p>
              <p class="mt-0.5 text-xs text-muted-foreground">Never delivered · carrier confirmed loss</p>
            </td>
            <td class="px-2 py-3 text-end align-top tabular-nums text-foreground">£12.50</td>
            <td class="px-5 py-3 text-end align-top">
              <span class="inline-flex items-center gap-1 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>Auto-approves in 3h</span>
            </td>
          </tr>
          <tr>
            <td class="px-5 py-3 align-top">
              <label for="approve-41833" class="sr-only">Refund order 41833</label>
              <input id="approve-41833" type="checkbox" class="h-4 w-4 rounded border-field accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" />
            </td>
            <td class="px-2 py-3 align-top">
              <p class="font-medium text-foreground">Refund order 41833</p>
              <p class="mt-0.5 text-xs text-muted-foreground">Duplicate charge · matched to payment intent pi_3Qk</p>
            </td>
            <td class="px-2 py-3 text-end align-top tabular-nums text-foreground">£64.00</td>
            <td class="px-5 py-3 text-end align-top">
              <span class="inline-flex items-center gap-1 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>Auto-approves in 6h</span>
            </td>
          </tr>
          <tr>
            <td class="px-5 py-3 align-top">
              <span aria-hidden="true" class="flex h-4 w-4 items-center justify-center text-muted-foreground">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-alert h-3.5 w-3.5" aria-hidden="true">
                  <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>
                  <path d="M12 8v4"></path>
                  <path d="M12 16h.01"></path>
                </svg>
              </span>
            </td>
            <td class="px-2 py-3 align-top">
              <p class="font-medium text-foreground">Close account for Meridian Foods</p>
              <p class="mt-0.5 text-xs text-muted-foreground">Requested by an admin · 41 users · 3 years of data</p>
              <p class="mt-1 inline-flex items-center gap-1.5 rounded-md bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert h-3 w-3" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>Deletes data — opens individually</p>
            </td>
            <td class="px-2 py-3 text-end align-top tabular-nums text-foreground">—</td>
            <td class="px-5 py-3 text-end align-top">
              <a href="#review" class="inline-flex items-center gap-1 rounded text-xs font-medium text-primary underline-offset-4 transition hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Open<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right h-3 w-3 rtl:rotate-180" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg><span class="sr-only"> Close account for Meridian Foods</span></a>
            </td>
          </tr>
          <tr>
            <td class="px-5 py-3 align-top">
              <label for="approve-41840" class="sr-only">Refund order 41840</label>
              <input id="approve-41840" type="checkbox" class="h-4 w-4 rounded border-field accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" />
            </td>
            <td class="px-2 py-3 align-top">
              <p class="font-medium text-foreground">Refund order 41840</p>
              <p class="mt-0.5 text-xs text-muted-foreground">Late delivery · goodwill, within policy</p>
            </td>
            <td class="px-2 py-3 text-end align-top tabular-nums text-foreground">£9.99</td>
            <td class="px-5 py-3 text-end align-top">
              <span class="inline-flex items-center gap-1 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>Auto-approves in 12h</span>
            </td>
          </tr>
          <tr>
            <td class="px-5 py-3 align-top">
              <span aria-hidden="true" class="flex h-4 w-4 items-center justify-center text-muted-foreground">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-alert h-3.5 w-3.5" aria-hidden="true">
                  <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>
                  <path d="M12 8v4"></path>
                  <path d="M12 16h.01"></path>
                </svg>
              </span>
            </td>
            <td class="px-2 py-3 align-top">
              <p class="font-medium text-foreground">Issue account credit to Halden Group</p>
              <p class="mt-0.5 text-xs text-muted-foreground">Outage compensation · agent proposed 3 months</p>
              <p class="mt-1 inline-flex items-center gap-1.5 rounded-md bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert h-3 w-3" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>Above the bulk limit — opens individually</p>
            </td>
            <td class="px-2 py-3 text-end align-top tabular-nums text-foreground">£4,200.00</td>
            <td class="px-5 py-3 text-end align-top">
              <a href="#review" class="inline-flex items-center gap-1 rounded text-xs font-medium text-primary underline-offset-4 transition hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Open<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right h-3 w-3 rtl:rotate-180" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg><span class="sr-only"> Issue account credit to Halden Group</span></a>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

7 more blocks in Human in the Loop

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Human in the Loop

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Human in the Loop blocks

View category
Open the full page for this block

Approval Request with Blast Radius

The agent stops and asks: the effect stated before the verb, choices as radios in a fieldset so picking is separate from committing, no pre-focused Approve, and the decision left on screen afterwards.

Human in the Loop247 lines1 dep
Open the full page for this block

Proposed Edits Diff Table

Machine-proposed row edits accepted one at a time, with before and after in real del/ins elements, a tri-state select-all that actually sets indeterminate, and a running count of what will be written.

Human in the Loop269 lines1 dep
Open the full page for this block

Recommendation with Confidence Meter

A suggestion scored with a native meter rather than a styled bar, banded in words as well as numbers, and showing what the model rejected — the fastest way a human catches a bad recommendation.

Human in the Loop258 lines1 dep
Open the full page for this block

Agent Permission Scopes

Read and write scopes separated rather than bundled, writes off by default, a required expiry, and switches that are real checkboxes with role="switch" — so the grant is toggleable by keyboard and announced as on or off.

Human in the Loop297 lines1 dep
Open the full page for this block

Approval Policy List

Which agent actions run unattended and which need a signature, stated as a policy the operator can read before anything happens.

Human in the Loop156 linesNo deps
Open the full page for this block

Escalation Queue

What is waiting on a person, ordered by how long it has been waiting rather than when it arrived.

Human in the Loop148 linesNo deps
Open the full page for this block

Human Oversight Split

Where a person sits in an automated run, written as four guarantees rather than as a promise that a human is involved.

Human in the Loop156 linesNo deps