OperantAI Workflow Design System
ComfortableCompact

ApprovalBar is the answer to AGENTS.md's decision-table row "Human decision on AI output" — never a toast. It renders a row of decision actions (Approve / Reject / Edit / Retry) and, in the React layer, wires global A/R/E keyboard shortcuts for as long as the bar is mounted. Each action is only rendered — and its shortcut only wired — when you pass the corresponding handler.

Installation

HTML — already included if you loaded the 3 stylesheets. See the Installation guide.

React

npm install @operant/ui

@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.

Usage

HTML

<div class="ods-approval-bar" role="group" aria-label="Review decision">
  <div class="grow">
    <div class="ods-run-name">Your decision</div>
    <div class="ods-approval-context">
      Logged with your identity + timestamp · reversible via re-deploy
    </div>
  </div>
  <button type="button" class="ods-btn -secondary">Edit <kbd>E</kbd></button>
  <button type="button" class="ods-btn -primary">Approve <kbd>A</kbd></button>
</div>

React

import { ApprovalBar, ApprovalContext } from "@operant/ui";

export default function Example() {
  return (
    <ApprovalBar onApprove={() => {}} onEdit={() => {}}>
      <div className="ods-run-name">Your decision</div>
      <ApprovalContext>
        Logged with your identity + timestamp · reversible via re-deploy
      </ApprovalContext>
    </ApprovalBar>
  );
}

In React, the A/E keys above are live: pressing them fires onApprove/onEdit as long as focus isn't inside an editable field. The plain-HTML version has no such behavior — see Accessibility.

Examples

Approve and reject

<div class="ods-approval-bar" role="group" aria-label="Review decision">
  <div class="grow">
    <div class="ods-run-name">Your decision</div>
    <div class="ods-approval-context">
      Logged with your identity + timestamp · reversible via re-deploy
    </div>
  </div>
  <button type="button" class="ods-btn -danger-ghost">Reject <kbd>R</kbd></button>
  <button type="button" class="ods-btn -primary">Approve <kbd>A</kbd></button>
</div>
import { ApprovalBar, ApprovalContext } from "@operant/ui";

<ApprovalBar onApprove={() => {}} onReject={() => {}}>
  <div className="ods-run-name">Your decision</div>
  <ApprovalContext>
    Logged with your identity + timestamp · reversible via re-deploy
  </ApprovalContext>
</ApprovalBar>;

All four actions

Reject uses -danger-ghost, Edit and Retry use -secondary, Approve uses -primary — matching the "one -primary per region" rule in AGENTS.md #9. Retry has no keyboard shortcut (it isn't wired to a letter key, and none of the hand-written example pages show a <kbd> next to it).

<div class="ods-approval-bar" role="group" aria-label="Review decision">
  <div class="grow">
    <div class="ods-run-name">Your decision</div>
    <div class="ods-approval-context">
      Logged with your identity + timestamp · reversible via re-deploy
    </div>
  </div>
  <button type="button" class="ods-btn -danger-ghost">Reject <kbd>R</kbd></button>
  <button type="button" class="ods-btn -secondary">Edit <kbd>E</kbd></button>
  <button type="button" class="ods-btn -secondary">Retry</button>
  <button type="button" class="ods-btn -primary">Approve <kbd>A</kbd></button>
</div>
import { ApprovalBar, ApprovalContext } from "@operant/ui";

<ApprovalBar
  onApprove={() => {}}
  onReject={() => {}}
  onEdit={() => {}}
  onRetry={() => {}}
>
  <div className="ods-run-name">Your decision</div>
  <ApprovalContext>
    Logged with your identity + timestamp · reversible via re-deploy
  </ApprovalContext>
</ApprovalBar>;

Disabled

disabled disables every rendered action button and unregisters the keyboard shortcuts.

<div class="ods-approval-bar" role="group" aria-label="Review decision">
  <div class="grow">
    <div class="ods-run-name">Your decision</div>
    <div class="ods-approval-context">Waiting on a prerequisite check…</div>
  </div>
  <button type="button" class="ods-btn -secondary" disabled>Edit <kbd>E</kbd></button>
  <button type="button" class="ods-btn -primary" disabled>Approve <kbd>A</kbd></button>
</div>
import { ApprovalBar, ApprovalContext } from "@operant/ui";

<ApprovalBar onApprove={() => {}} onEdit={() => {}} disabled>
  <div className="ods-run-name">Your decision</div>
  <ApprovalContext>Waiting on a prerequisite check…</ApprovalContext>
</ApprovalBar>;

Under a proposed diff

The bar's most common placement: sitting directly under an .ods-diff block, so the decision is made in context.

<section class="stack gap-6">
  <div class="text-eyebrow">Proposed change · deployment manifest</div>
  <div class="ods-diff">
    <div class="ods-diff-line -del">
      <span class="ln">12</span><span class="code"> image: checkout-svc:v2.31.0</span>
    </div>
    <div class="ods-diff-line -add">
      <span class="ln">12</span><span class="code"> image: checkout-svc:v2.30.4</span>
    </div>
  </div>
</section>
<div class="ods-approval-bar" role="group" aria-label="Review decision">
  <div class="grow">
    <div class="ods-run-name">Your decision</div>
    <div class="ods-approval-context">
      Logged with your identity + timestamp · reversible via re-deploy
    </div>
  </div>
  <button type="button" class="ods-btn -danger-ghost">Reject <kbd>R</kbd></button>
  <button type="button" class="ods-btn -secondary">Edit <kbd>E</kbd></button>
  <button type="button" class="ods-btn -secondary">Retry</button>
  <button type="button" class="ods-btn -primary">Approve <kbd>A</kbd></button>
</div>
import { ApprovalBar, ApprovalContext } from "@operant/ui";

<div className="stack gap-12">
  <section className="stack gap-6">
    <div className="text-eyebrow">Proposed change · deployment manifest</div>
    <div className="ods-diff">
      <div className="ods-diff-line -del">
        <span className="ln">12</span>
        <span className="code"> image: checkout-svc:v2.31.0</span>
      </div>
      <div className="ods-diff-line -add">
        <span className="ln">12</span>
        <span className="code"> image: checkout-svc:v2.30.4</span>
      </div>
    </div>
  </section>
  <ApprovalBar
    onApprove={() => {}}
    onReject={() => {}}
    onEdit={() => {}}
    onRetry={() => {}}
  >
    <div className="ods-run-name">Your decision</div>
    <ApprovalContext>
      Logged with your identity + timestamp · reversible via re-deploy
    </ApprovalContext>
  </ApprovalBar>
</div>;

API Reference

<ApprovalBar>

PropTypeDefaultDescription
childrenReactNode—Rendered inside the leading .grow slot — typically an .ods-run-name title plus an ApprovalContext.
onApprove() => void—Renders the Approve button and wires the A shortcut. Omit to hide the action entirely.
onReject() => void—Renders the Reject button and wires the R shortcut.
onEdit() => void—Renders the Edit button and wires the E shortcut.
onRetry() => void—Renders the Retry button. No dedicated keyboard shortcut.
approveLabelstring"Approve"Approve button label.
rejectLabelstring"Reject"Reject button label.
editLabelstring"Edit"Edit button label.
retryLabelstring"Retry"Retry button label.
disabledbooleanfalseDisables every rendered button and unregisters the A/R/E keyboard handlers.
rolestring"group"Overridable via native HTMLAttributes.
aria-labelstring"Review decision"Overridable via native HTMLAttributes.

<ApprovalContext>

Thin wrapper rendering a .ods-approval-context <div> — accepts all native div attributes. No dedicated props.

HTML-side modifiers: the bar itself has no variant classes — only the base .ods-approval-bar. Style the action buttons with the normal Button classes: -primary for Approve, -secondary for Edit/Retry, -danger-ghost for Reject.

Accessibility

  • The bar has role="group" and aria-label="Review decision" by default, both overridable.
  • React only: while mounted, a window keydown listener maps a/r/e to onApprove/onReject/onEdit. The listener ignores keypresses when the event target is contentEditable, an <input>, <textarea>, or <select>, and when any of Cmd/Ctrl/Alt is held — so it never steals keystrokes from a text field. disabled unregisters the listener entirely.
  • The plain-HTML markup above renders the same visual bar but does not get these shortcuts for free — they're a useEffect in the React component. Wire your own keydown listener with the same guard if you need shortcut parity outside React.
  • <kbd>R</kbd>/<kbd>E</kbd>/<kbd>A</kbd> are visual hints only; they don't grant focus or activate on their own.
  • Each button is a real <button type="button">, so it's keyboard-focusable and announces its label plus the visible key hint as part of its accessible name.