agentic / 11

Installation

tokens.css + approval-gate.css + button.css
head.html
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components/approval-gate/approval-gate.css">

Basic

approval-gate / Basicinteractive

Review required

Write release-notes.md?

⌘↵ Approve

write-file · release-notes.md · actionId=write-release-notes-01 · exact action shown

Why this is safe

Draft only touches a new markdown file. No secrets or production paths.

Code

approval-gate.html + approval-gate.css
approval-gate
<style>
.au-approval-gate { background: var(--au-surface); border: 1px solid var(--au-border-color); border-radius: var(--au-radius-xl); min-width: 0; overflow: hidden; }
.au-approval-gate__header { align-items: start; display: flex; gap: 16px; justify-content: space-between; padding: 16px 20px; }
.au-approval-gate__eyebrow,.au-approval-gate__rationale-label { color: var(--au-fg-muted); font: 400 12px/1.3 var(--au-font-sans); margin: 0 0 8px; }
.au-approval-gate__eyebrow { color: var(--au-warning); }
.au-approval-gate__title { font-size: 16px; font-weight: 600; line-height: 1.3; margin: 0; }
.au-approval-gate__aside { align-items: center; display: flex; flex-shrink: 0; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
.au-approval-gate__shortcut { background: var(--au-surface-muted); border: 1px solid var(--au-border-color); border-radius: var(--au-radius-pill); color: var(--au-fg-subtle); font: 400 12px/1.3 var(--au-font-sans); padding: 4px 8px; white-space: nowrap; }
.au-approval-gate__action { background: var(--au-surface-muted); border-top: 1px solid var(--au-border-color); color: var(--au-fg-muted); font: 400 13px/1.5 var(--au-font-sans); margin: 0; padding: 12px 20px; }
.au-approval-gate__action code { font: 12px/1.4 var(--au-font-mono); }
.au-approval-gate__rationale { border-top: 1px solid var(--au-border-color); padding: 16px 20px; }
.au-approval-gate__rationale-label { color: var(--au-fg-subtle); }
.au-approval-gate__rationale-body { color: var(--au-fg-muted); font-size: 14px; line-height: 1.5; margin: 0; }
.au-approval-gate__footer { border-top: 1px solid var(--au-border-color); display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; min-width: 0; padding: 12px 20px; }
@media (max-width: 480px) {
  .au-approval-gate__header { flex-wrap: wrap; }
  .au-approval-gate__footer { justify-content: stretch; }
  .au-approval-gate__footer .au-btn { flex: 1 1 auto; }
}
.au-approval-gate[data-au-state="success"] { border-color: var(--au-success); }
.au-approval-gate[data-au-state="error"] { border-color: var(--au-danger); }

/* --- Confirmed decision state (adapted from 21st.dev tool-approval-footer) ---
   When a decision is made, the host sets data-au-state="approved" or "rejected"
   to reflect the outcome instead of leaving the footer visually unchanged.
   Host-driven (no-JS friendly): set the attribute after the <form> submits;
   buttons keep native submit semantics so nothing requires a script to work.
   The decision line is hidden by default and revealed only in its state. */
.au-approval-gate__decision {
  align-items: center;
  color: var(--au-fg-muted);
  display: none;
  font: 400 12px/1.3 var(--au-font-sans);
  gap: 8px;
}
.au-approval-gate__decision-dots { display: inline-flex; }
.au-approval-gate__decision-dots i {
  animation: au-approval-dots 1.4s infinite;
  display: inline-block;
  font-style: normal;
}
.au-approval-gate__decision-dots i + i { animation-delay: 0.2s; }
.au-approval-gate__decision-dots i + i + i { animation-delay: 0.4s; }

.au-approval-gate[data-au-state="approved"] {
  border-color: var(--au-success);
}
.au-approval-gate[data-au-state="approved"] .au-approval-gate__decision {
  color: var(--au-success);
  display: flex;
}
.au-approval-gate[data-au-state="rejected"] {
  border-color: var(--au-danger);
}
.au-approval-gate[data-au-state="rejected"] .au-approval-gate__decision {
  color: var(--au-danger);
  display: flex;
}
.au-approval-gate[data-au-state="approved"] .au-btn,
.au-approval-gate[data-au-state="rejected"] .au-btn {
  opacity: var(--au-disabled-opacity);
  pointer-events: none;
}

@keyframes au-approval-dots {
  0%, 20% { opacity: 0.2; }
  50% { opacity: 1; }
  100% { opacity: 0.2; }
}
@media (prefers-reduced-motion: reduce) {
  .au-approval-gate__decision-dots i { animation: none; }
}
</style>

<section class="au-approval-gate" data-au-state="waiting" data-action-id="write-release-notes-01" aria-labelledby="approval-preview-title"><header class="au-approval-gate__header"><div><p class="au-approval-gate__eyebrow">Review required</p><h2 class="au-approval-gate__title" id="approval-preview-title">Write release-notes.md?</h2></div><div class="au-approval-gate__aside"><span class="au-approval-gate__shortcut">⌘↵ Approve</span></div></header><p class="au-approval-gate__action">write-file · release-notes.md · <code>actionId=write-release-notes-01</code> · exact action shown</p><div class="au-approval-gate__rationale"><p class="au-approval-gate__rationale-label">Why this is safe</p><p class="au-approval-gate__rationale-body">Draft only touches a new markdown file. No secrets or production paths.</p></div><form class="au-approval-gate__footer" method="post"><input type="hidden" name="actionId" value="write-release-notes-01"><input type="hidden" name="args" value="{&quot;path&quot;:&quot;release-notes.md&quot;}"><button class="au-btn au-btn--ghost" type="submit" name="decision" value="reject">Keep editing</button><button class="au-btn au-btn--primary" type="submit" name="decision" value="approve">Approve write</button></form></section>

Component contract

Install
tokens.css · approval-gate.css · button.css
States
waiting, success, error, cancelled, approved, rejected — approved/rejected
Host
authorization, recording, and execution
For agents
approval-gate.contract
# agent-ui / approval-gate
component: approval-gate
selector: .au-approval-gate
data_au_component: approval-gate
purpose: A bounded action proposal that requires a human decision before execution.

install:
  - tokens.css
  - approval-gate.css
  - button.css for actions

states: waiting, success, error, cancelled, approved, rejected — approved/rejected are host-driven and reveal the decision line.
css_variables: Uses semantic tokens.
events: native form, focus, and click only — no custom events or runtime required
no_javascript: Native decision form remains submittable.
host_owns: The host owns authorization, recording, and execution.
accessibility: Expose the exact action as text; never color-only status.
security: Bind approve/reject to immutable action identity and exact args.

rules:
  - Host owns execution, authorization, persistence, validation, and state transitions.
  - Components render host-provided state; they never execute tools or imply approval.
  - Escape untrusted model output, tool args, and tool output before insertion.
  - Use only documented data-au-* attributes and data-au-state values.
  - Keep the no-JS path usable. Do not add React, Tailwind, bundlers, or remote assets.
source: components/approval-gate/approval-gate.{html,css} + README.md + fixture.html