Installation
tokens.css + approval-gate.css + button.csshead.html
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components/approval-gate/approval-gate.css">Basic
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="{"path":"release-notes.md"}"><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