Live CortexUI Surface

This block renders live CortexUI contract metadata in the docs DOM so AI View can inspect real machine-readable elements instead of only code examples.

AI View can now inspect a live status region, form fields, actions, and table entities on every docs page.
AI-addressable docs entities
ItemState
Search docsReady
Inspect metadataVisible in AI View

ActionButton

The primary interactive element in CortexUI. ActionButton is not just a button — it is a stateful, machine-readable action trigger that declares its identity, its operation, and its current state to both human users and AI agents simultaneously.

Overview

Use ActionButton whenever you need a user-facing trigger that initiates an operation. It is the correct choice for form submissions, data mutations, navigation triggers, and any action that has a meaningful machine state lifecycle.

Key features:

  • Declares a stable action identifier that AI agents can target deterministically
  • Exposes machine state (idle, loading, success, error, disabled) as a DOM attribute
  • Handles the loading and disabled states visually and semantically
  • Prevents double-submission automatically when in loading state
  • Supports primary, secondary, and danger variants
Important

ActionButton is the canonical way to trigger operations in CortexUI. Do not use plain <button> elements for operations that AI agents need to identify or that have async state lifecycles. An unwrapped button has no AI contract.

Anatomy

An ActionButton renders a single <button> element with two complete layers:

┌─────────────────────────────────────────────────────────────┐
│  ActionButton                                               │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  <button>                                           │   │
│  │    Visual layer: variant styles, loading spinner,   │   │
│  │                  disabled opacity                   │   │
│  │    Semantic layer: data-ai-role, data-ai-action,    │   │
│  │                    data-ai-state, data-ai-id        │   │
│  │                                                     │   │
│  │    [ label text or loading indicator ]              │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Parts:

  • Root element — a native <button> with all ARIA and semantic attributes applied
  • Label — the visible text content passed as label or children
  • Loading indicator — rendered automatically when state="loading"
  • Semantic contractdata-ai-* attributes always present on the root element

Props

PropTypeDefaultDescription
actionstring(required)The logical operation identifier. Used as data-ai-action and data-ai-id. Must be unique within a screen.
state"idle" | "loading" | "success" | "error" | "disabled"(required)The current machine state of the action. Drives both visual rendering and the data-ai-state attribute.
labelstringundefinedThe button's visible text label. If omitted, renders children.
variant"primary" | "secondary" | "danger""primary"Controls visual styling. danger renders in a destructive color and implies a high-risk action.
disabledbooleanfalseDisables the button. When true, also sets state to "disabled" in the semantic layer.
loadingbooleanfalseShorthand for state="loading".
onAction() => voidundefinedCallback fired when the button is clicked and not in a loading or disabled state.
childrenReactNodeundefinedAlternative to label for custom button content.

AI Contract

ActionButton publishes the following data-ai-* attributes on every render:

AttributeValuePurpose
data-ai-role"action"Declares this element as an action trigger. Agents filter by this to find all clickable operations.
data-ai-actionValue of the action propNames the logical operation. Agents use this to find the right trigger by operation name.
data-ai-stateCurrent value of the state propExposes machine state. Agents check this before deciding to click — they will not trigger a loading or disabled button.
data-ai-idValue of the action propStable unique identifier for this specific element.

Rendered example:

<button
  data-ai-role="action"
  data-ai-action="save-profile"
  data-ai-state="idle"
  data-ai-id="save-profile"
  class="btn btn-primary"
>
  Save Profile
</button>

During loading:

<button
  data-ai-role="action"
  data-ai-action="save-profile"
  data-ai-state="loading"
  data-ai-id="save-profile"
  class="btn btn-primary btn-loading"
  disabled
>
  Save Profile
</button>

An AI agent waiting for an async operation to complete can observe the data-ai-state attribute transition from loading to success or error without any visual parsing.

States

idle

The default resting state. The button is ready to be activated.

<ActionButton
  action="save-profile"
  state="idle"
  label="Save Profile"
  onAction={handleSave}
/>

loading

The operation is in progress. The button renders a loading indicator and is non-interactive.

<ActionButton
  action="save-profile"
  state="loading"
  label="Save Profile"
/>

success

The operation completed successfully. Typically shown briefly before resetting to idle.

<ActionButton
  action="save-profile"
  state="success"
  label="Save Profile"
/>

error

The operation failed. The button typically resets to idle after a short delay or on user interaction.

<ActionButton
  action="save-profile"
  state="error"
  label="Save Profile"
/>

disabled

The action is not available in the current context. Visually dimmed and non-interactive.

<ActionButton
  action="save-profile"
  state="disabled"
  label="Save Profile"
  disabled
/>

State progression

The typical lifecycle of an async action:

function SaveProfileButton() {
  const [state, setState] = useState<ActionState>("idle");

  async function handleSave() {
    setState("loading");
    try {
      await api.saveProfile(profileData);
      setState("success");
      // Reset to idle after 2s
      setTimeout(() => setState("idle"), 2000);
    } catch (err) {
      setState("error");
      setTimeout(() => setState("idle"), 3000);
    }
  }

  return (
    <ActionButton
      action="save-profile"
      state={state}
      label="Save Profile"
      onAction={handleSave}
    />
  );
}

This state progression is machine-observable. An AI agent watching the DOM sees: idleloadingsuccessidle

Examples

Primary form submission

function CheckoutForm() {
  const [submitState, setSubmitState] = useState<ActionState>("idle");

  async function handleSubmit() {
    setSubmitState("loading");
    try {
      await api.submitOrder(cart);
      setSubmitState("success");
    } catch {
      setSubmitState("error");
    }
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      {/* form fields */}
      <ActionButton
        action="submit-order"
        state={submitState}
        label="Place Order"
        variant="primary"
        onAction={handleSubmit}
      />
    </form>
  );
}

Destructive action with danger variant

<ActionButton
  action="delete-account"
  state={deleteState}
  label="Delete Account"
  variant="danger"
  onAction={handleDeleteAccount}
/>

Secondary action

<ActionButton
  action="export-report"
  state={exportState}
  label="Export CSV"
  variant="secondary"
  onAction={handleExport}
/>

Multiple actions in the same view

<Stack direction="horizontal" gap={2}>
  <ActionButton
    action="save-draft"
    state={draftState}
    label="Save Draft"
    variant="secondary"
    onAction={saveDraft}
  />
  <ActionButton
    action="publish-post"
    state={publishState}
    label="Publish"
    variant="primary"
    onAction={publishPost}
  />
</Stack>

Both buttons are individually queryable by an AI agent using their action identifiers: "save-draft" and "publish-post".

Conditionally disabled

<ActionButton
  action="submit-order"
  state={cartIsEmpty ? "disabled" : submitState}
  label="Place Order"
  disabled={cartIsEmpty}
  onAction={handleSubmit}
/>

Accessibility

  • Renders as a native <button> element — keyboard accessible by default
  • Enter and Space activate the button, consistent with browser native behavior
  • When state="loading" or disabled, the button has the disabled HTML attribute, preventing activation via keyboard
  • Use aria-label via the label prop when the button text alone is ambiguous (e.g., an icon-only button)
  • The data-ai-state attribute does not affect ARIA semantics — it is a parallel layer
Note

ActionButton is built on top of ButtonBase, which handles all keyboard interaction, focus management, and ARIA attribute wiring. You do not need to add ARIA manually.

Anti-patterns

Using a div instead of button

// WRONG: A div has no keyboard accessibility and no semantic role
<div
  className="btn btn-primary"
  onClick={handleSave}
>
  Save
</div>

// RIGHT: Use ActionButton, which renders a real <button>
<ActionButton action="save" state="idle" label="Save" onAction={handleSave} />

Not providing a state

// WRONG: state is required. Omitting it breaks the AI contract.
<ActionButton action="save" label="Save" onAction={handleSave} />

// RIGHT: Always pass the current state explicitly
<ActionButton action="save" state={saveState} label="Save" onAction={handleSave} />

Using a button for navigation

// WRONG: Navigation is not an action — use a link
<ActionButton action="go-to-profile" state="idle" label="Profile" onAction={() => router.push("/profile")} />

// RIGHT: Use a link element or NavigationItem for navigation
<a href="/profile">Profile</a>

Hardcoding loading state

// WRONG: state is static — the AI contract never updates
<ActionButton action="save" state="loading" label="Save" />

// RIGHT: Derive state from actual async operation status
const [state, setState] = useState<ActionState>("idle");
<ActionButton action="save" state={state} label="Save" onAction={handleSave} />

Reusing action identifiers across screens

// WRONG: Two different "submit" buttons with the same action on different screens
// are fine, but two buttons on the same screen should have distinct actions.
<ActionButton action="submit" state="idle" label="Save Draft" />
<ActionButton action="submit" state="idle" label="Publish" />

// RIGHT: Each distinct operation gets a distinct action identifier
<ActionButton action="save-draft" state={draftState} label="Save Draft" />
<ActionButton action="publish-post" state={publishState} label="Publish" />