# Option list
URL: /elements/option-list

The agent asks a question with a few answers; the pick returns to it and stays as a receipt.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

An option list puts the answers to the agent's question in front of the user as rows. A single pick commits on the spot; a multiple selection gathers checks and commits on confirm. Once the answer lands, the list collapses to a receipt of just the chosen rows, so scrolling back never shows a live control for a decision already made.

## Getting started

**With a runtime:**

The question is a human tool: the model supplies the options as arguments, the run pauses, and `addResult` sends the pick back as the tool's result. The same result drives the receipt.

1. ### Render the tool call

   ```
   "use client";

   import { defineToolkit, useAuiState } from "@assistant-ui/react";
   import { z } from "zod";
   import { OptionList } from "@/components/assistant-ui/elements/option-list";

   export const toolkit = defineToolkit({
     ask_user_to_choose: {
       type: "human",
       description:
         "Ask the user a question with a few answers. Put the question itself in your message, not in the options.",
       parameters: z.object({
         options: z.array(
           z.object({
             id: z.string(),
             label: z.string(),
             description: z.string().optional(),
           }),
         ),
         selectionMode: z.enum(["single", "multiple"]).optional(),
       }),
       render: function AskUserToChoose({ args, status, result, addResult }) {
         const canAnswer = useAuiState(
           (s) => s.thread.capabilities.answerToolCall,
         );
         const waiting = status.type === "requires-action" && canAnswer;

         return (
           <OptionList
             options={args.options ?? []}
             selectionMode={args.selectionMode}
             choice={result?.selected}
             onConfirm={
               waiting ? (ids) => addResult({ selected: ids }) : undefined
             }
           />
         );
       },
     },
   });
   ```

   `status.type` is `"requires-action"` only once the arguments are complete and the run is waiting on the user, and `answerToolCall` is `false` in a readonly thread such as a sub-agent transcript. Outside that window `onConfirm` is absent and the list only displays its options.

2. ### Register the toolkit

   ```
   "use client";

   import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
   import { useChatRuntime } from "@assistant-ui/ai-sdk";
   import { toolkit } from "./toolkit";

   const config = AuiConfig({ tools: Tools({ toolkit }) });

   export function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
     const runtime = useChatRuntime();
     return (
       <AssistantRuntimeProvider runtime={runtime} config={config}>
         {children}
       </AssistantRuntimeProvider>
     );
   }
   ```

   A human tool has no executor on either side, so the model only sees its answer as the tool result `{ selected: [...] }`. See [Tool UI](/docs/tools/tool-ui#user-input-collection) for the human tool lifecycle.

**Standalone (no runtime):**

Standalone, `onConfirm` receives the pick and a fulfilled confirmation settles into a receipt. `choice` lets your application supply the receipt and takes precedence when it is set.

1. ### Hold the answer

   ```
   "use client";

   import { useState } from "react";
   import { OptionList } from "@/components/assistant-ui/elements/option-list";

   const OPTIONS = [
     { id: "merge", label: "Merge duplicates" },
     { id: "keep", label: "Keep both" },
     { id: "review", label: "Review each pair" },
   ];

   export function Duplicates() {
     const [choice, setChoice] = useState<string[]>();

     return (
       <OptionList
         aria-label="How should I handle the duplicate contacts?"
         options={OPTIONS}
         choice={choice}
         onConfirm={setChoice}
       />
     );
   }
   ```

2. ### Commit asynchronously

   Return a promise from `onConfirm` when the answer has to reach a server first. The list locks while it is pending, then shows its receipt when the promise resolves even without `choice`. A supplied `choice` still takes precedence. A rejection reopens the list with the error message, because the answer never landed:

   ```
   <OptionList
     options={OPTIONS}
     choice={choice}
     onConfirm={async (ids) => {
       await saveAnswer(ids);
       setChoice(ids);
     }}
   />
   ```

## Anatomy

```
<div data-slot="option-list" data-state="open | pending | receipt">
  {/* one row per option: a button, a checkbox in multiple mode, or a plain row when display only */}
  {/* multiple mode: a "2 of 3" count and the confirm button */}
</div>
```

The list has three shapes. Without `onConfirm` it only displays its options. With `onConfirm` a single pick commits on the spot, and a multiple selection toggles checkboxes and commits on the confirm button. A fulfilled confirmation becomes a receipt. `choice` supplies and takes precedence over that receipt: only the chosen options remain, each marked selected, and nothing takes input. An empty `choice` reads "Nothing selected".

Disabled options stay visible but never commit. In multiple mode, once `maxSelections` options are checked the remaining ones are unavailable until one is unchecked, and the confirm button stays unavailable until at least `minSelections` are checked.

## Examples

### Several answers

`selectionMode="multiple"` swaps the rows for checkboxes and adds a confirm step. `defaultValue` pre-checks options, and `minSelections` and `maxSelections` bound the answer:

```
<OptionList
  aria-label="Which checks should run before the deploy?"
  selectionMode="multiple"
  defaultValue={["typecheck", "unit"]}
  maxSelections={3}
  options={CHECKS}
  onConfirm={setChoice}
/>
```

The ids reach `onConfirm` in the order of `options`, not the order they were checked.

### Label the question

The question belongs in the assistant's message, not in the list, so the list carries no heading. Give the group an accessible name with `aria-label` or `aria-labelledby` so a screen reader announces what it answers.

## API reference

**With a runtime:**

### Tool-call render props

| Prop        | Type                                                                      | Description                                                            |
| ----------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `args`      | `{ options: OptionListOption[]; selectionMode?: "single" \| "multiple" }` | The options the model offered, partial while they stream.              |
| `status`    | `ToolCallMessagePartStatus`                                               | `status.type === "requires-action"` while the run waits on the answer. |
| `result`    | `{ selected: string[] } \| undefined`                                     | The committed answer, which becomes `choice`.                          |
| `addResult` | `(result) => void`                                                        | Completes the tool call with the answer.                               |

See [Tool UI](/docs/tools/tool-ui) for the full render-prop surface.

**Standalone (no runtime):**

### OptionList

| Prop            | Type                                       | Default          | Description                                                                                                                           |
| --------------- | ------------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `options`       | `readonly OptionListOption[]`              | required         | `{ id, label, description?, disabled? }` rows, in display order.                                                                      |
| `selectionMode` | `"single" \| "multiple"`                   | `"single"`       | A single pick commits on the spot; a multiple selection commits on confirm.                                                           |
| `defaultValue`  | `readonly string[]`                        |                  | Ids the list starts selected: checked in a multiple selection, marked as the current answer in a single one. Unknown ids are ignored. |
| `minSelections` | `number`                                   | `1`              | The fewest options a multiple selection confirms with.                                                                                |
| `maxSelections` | `number`                                   | `options.length` | The most options a multiple selection may hold.                                                                                       |
| `onConfirm`     | `(ids: string[]) => void \| Promise<void>` |                  | Commits the answer. A fulfilled confirmation becomes a receipt; without it the list only displays its options.                        |
| `confirmLabel`  | `string`                                   | `"Confirm"`      | The confirm button's label in multiple mode.                                                                                          |
| `choice`        | `string[]`                                 |                  | The committed answer. When set, it takes precedence and the list renders as a receipt.                                                |
| `className`     | `string`                                   |                  | Merged onto the root.                                                                                                                 |

All other `div` props, including `aria-label`, are forwarded to the root.