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

State Visibility

State visibility is the principle that the current condition of every meaningful UI element must be explicitly declared in the DOM — not inferred from visual styling, CSS class names, or computed layout.

Why State Must Be Explicit

Human beings infer state from visual cues. A grayed-out button with 50% opacity reads as disabled. A spinning icon reads as loading. A green background reads as success. These visual cues are meaningful to human perception — but they are completely opaque to AI agents, automated tests, and accessibility tools.

An AI agent cannot see colors. It cannot interpret opacity. It does not know that class="btn--loading" means the button is in a loading state. It reads the DOM.

State visibility solves this by making every meaningful condition a first-class attribute on the element:

<!-- State hidden in CSS: AI cannot read this -->
<button class="btn btn--loading btn--disabled" style="opacity: 0.6">
  Processing...
</button>

<!-- State explicit in data-ai-state: AI can read this -->
<button
  data-ai-state="loading"
  data-ai-role="action"
  data-ai-id="submit-order"
  disabled
  aria-busy="true"
>
  Processing...
</button>

In the first example, an AI agent sees a button element. In the second, it sees a button that is currently loading.

The 7 States

StateMeaningWhen to Apply
idleReady for interactionDefault state; after reset from any other state
loadingAsync operation in progressImmediately when an operation begins
successOperation completed successfullyImmediately when an operation resolves
errorOperation failedImmediately when an operation rejects
disabledNot currently interactableWhen permission is absent or prerequisites unmet
expandedAn overlay or collapsible is openWhen modal, dropdown, or accordion opens
selectedThe element is in an active/chosen stateFor tabs, filters, toggles, list selections

Anti-pattern: State Hidden in CSS Class Names

This is the most common violation of state visibility. Developers add CSS classes to convey state — but CSS classes are not part of the AI contract.

// Wrong: state in class names only
function SubmitButton({ isLoading, isDisabled }) {
  return (
    <button
      className={`btn ${isLoading ? "btn--loading" : ""} ${isDisabled ? "btn--disabled" : ""}`}
      disabled={isDisabled}
    >
      {isLoading ? "Submitting..." : "Submit"}
    </button>
  );
}

// Correct: state in data-ai-state
function SubmitButton({ isLoading, isDisabled }) {
  const state = isDisabled ? "disabled" : isLoading ? "loading" : "idle";

  return (
    <button
      data-ai-role="action"
      data-ai-id="submit-order"
      data-ai-action="submit-order"
      data-ai-state={state}
      className={`btn ${isLoading ? "btn--loading" : ""} ${isDisabled ? "btn--disabled" : ""}`}
      disabled={isDisabled || isLoading}
      aria-busy={isLoading}
      aria-disabled={isDisabled}
    >
      {isLoading ? "Submitting..." : "Submit"}
    </button>
  );
}
Important

CSS class names are an implementation detail. They can change during a design refresh without any semantic meaning — "btn--loading" might become "button-busy" or "is-pending". data-ai-state is a contract term. It must not change without a versioned migration.

State Transitions

State transitions are the moments when one state gives way to another. They must be synchronous with the underlying operation — the state change and the operation start on the same tick of the event loop.

Button: idle → loading → success

async function handleSubmit() {
  // Transition 1: idle → loading (synchronous)
  setState("loading");

  try {
    await submitOrder(cartData);

    // Transition 2: loading → success (on resolve)
    setState("success");
    setResult("success");
  } catch (err) {
    // Transition 3: loading → error (on reject)
    setState("error");
    setResult(err.code ?? "unknown-error");
  }
}
[idle] ──click──► [loading] ──resolve──► [success]
                            ──reject───► [error]
[success] ──3s timer──► [idle]
[error] ──user retry──► [idle]

Field: idle → error (on validation failure)

function EmailField({ value, onChange }) {
  const [state, setState] = useState("idle");

  function handleBlur() {
    if (!isValidEmail(value)) {
      setState("error");
    } else {
      setState("idle");
    }
  }

  return (
    <input
      data-ai-role="field"
      data-ai-id="email-field"
      data-ai-field-type="email"
      data-ai-required="true"
      data-ai-state={state}
      aria-invalid={state === "error"}
      type="email"
      value={value}
      onChange={onChange}
      onBlur={handleBlur}
    />
  );
}
function ConfirmDeleteModal({ isOpen, onConfirm, onCancel }) {
  if (!isOpen) return null; // Not in DOM when closed

  return (
    <dialog
      data-ai-role="modal"
      data-ai-id="confirm-delete-modal"
      data-ai-state="expanded"
      open
      aria-modal="true"
    >
      <p>Are you sure you want to delete this record?</p>
      <button
        data-ai-role="action"
        data-ai-id="confirm-delete"
        data-ai-action="delete-record"
        data-ai-state="idle"
        onClick={onConfirm}
      >
        Delete
      </button>
      <button
        data-ai-role="action"
        data-ai-id="cancel-delete"
        data-ai-action="cancel-delete"
        data-ai-state="idle"
        onClick={onCancel}
      >
        Cancel
      </button>
    </dialog>
  );
}

Example: A Button Going idle → loading → success

Here is the complete lifecycle of a "Save Profile" button, annotated at each stage:

<!-- Stage 1: idle — ready for interaction -->
<button
  data-ai-role="action"
  data-ai-id="save-profile"
  data-ai-action="save-profile"
  data-ai-state="idle"
>
  Save Profile
</button>

<!-- Stage 2: loading — user clicked, request in flight -->
<button
  data-ai-role="action"
  data-ai-id="save-profile"
  data-ai-action="save-profile"
  data-ai-state="loading"
  disabled
  aria-busy="true"
>
  Saving...
</button>

<!-- Stage 3: success — request resolved -->
<button
  data-ai-role="action"
  data-ai-id="save-profile"
  data-ai-action="save-profile"
  data-ai-state="success"
  data-ai-result="success"
>
  Saved!
</button>

<!-- Stage 4: idle again — after reset timer -->
<button
  data-ai-role="action"
  data-ai-id="save-profile"
  data-ai-action="save-profile"
  data-ai-state="idle"
>
  Save Profile
</button>

How AI Agents Use State

State is how agents know whether an action is currently available, in progress, or completed. Before triggering any action, an agent checks its state:

const actions = window.__CORTEX_UI__.getAvailableActions();
const saveAction = actions.find(a => a.action === "save-profile");

if (!saveAction) {
  throw new Error("save-profile not found on this screen");
}

if (saveAction.state === "loading") {
  // Wait for the operation already in progress to resolve
  await waitForActionState("save-profile", ["success", "error"]);
}

if (saveAction.state === "disabled") {
  throw new Error("save-profile is currently disabled — prerequisites not met");
}

if (saveAction.state === "idle") {
  // Safe to trigger
  triggerAction("save-profile");
}

State and Accessibility

data-ai-state and ARIA attributes are parallel — both must be set. They serve different consumers.

<!-- Complete: both AI contract and accessibility attributes -->
<button
  data-ai-role="action"
  data-ai-id="save-profile"
  data-ai-action="save-profile"
  data-ai-state="loading"
  disabled
  aria-busy="true"
  aria-disabled="true"
>
  Saving...
</button>

<input
  data-ai-role="field"
  data-ai-id="email-field"
  data-ai-field-type="email"
  data-ai-state="error"
  aria-invalid="true"
  aria-describedby="email-error"
/>

<button
  data-ai-role="action"
  data-ai-id="filter-active"
  data-ai-action="filter-active"
  data-ai-state="selected"
  aria-pressed="true"
>
  Active
</button>
Best Practice

Think of data-ai-state and ARIA as two different translation layers on top of the same underlying truth. The underlying truth is "this button is currently loading." data-ai-state translates that truth for AI agents. aria-busy translates it for screen readers. Both translations must happen simultaneously.