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>
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | 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. |
approveLabel | string | "Approve" | Approve button label. |
rejectLabel | string | "Reject" | Reject button label. |
editLabel | string | "Edit" | Edit button label. |
retryLabel | string | "Retry" | Retry button label. |
disabled | boolean | false | Disables every rendered button and unregisters the A/R/E keyboard handlers. |
role | string | "group" | Overridable via native HTMLAttributes. |
aria-label | string | "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"andaria-label="Review decision"by default, both overridable. - React only: while mounted, a
windowkeydownlistener mapsa/r/etoonApprove/onReject/onEdit. The listener ignores keypresses when the event target iscontentEditable, an<input>,<textarea>, or<select>, and when any of Cmd/Ctrl/Alt is held — so it never steals keystrokes from a text field.disabledunregisters the listener entirely. - The plain-HTML markup above renders the same visual bar but does not get these shortcuts for free — they're a
useEffectin the React component. Wire your ownkeydownlistener 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.