> For AI agents: the complete documentation index is available at https://ooui-react.pages.dev/en/llms.txt, the full documentation bundle is available at https://ooui-react.pages.dev/en/llms-full.txt.

# Controlled and uncontrolled

Most components' "current value" can be held by you (controlled) or kept by the component itself (uncontrolled).

## Controlled channels

| Channel    | Props                                     | Used by                                                                                      |
| ---------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- |
| Value      | `value` / `defaultValue` / `onChange`     | Inputs, selects, tag inputs, file pickers, and the active item of layouts                    |
| Open state | `open` / `defaultOpen` / `onOpenChange`   | `Popup`, `PopupButton`, `ButtonMenuSelectWidget`, toolbar popups and a tool's `popup` config |
| Checked    | `checked` / `defaultChecked` / `onChange` | `CheckboxInput`, `RadioInput`, `ToggleButton`, `ToggleSwitch`                                |

For the option data convention shared by the select family (`value`
, group headings, `labelText`
, disabled propagation, etc.), see [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
. 
## How control is decided

**Passing `value` (or `checked`) makes it controlled**, and the component renders exactly the value you pass; if you don't pass it, the component keeps its own internal state, with `defaultValue` used only as the initial value.

```tsx preview
import { useState } from "react";
import { Button, ToggleSwitch } from "ooui-react";

function App() {
  const [on, setOn] = useState(false);

  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 12 }}>
      {/* Controlled: the value is held outside */}
      <ToggleSwitch checked={on} onChange={(checked) => setOn(checked)} />
      {/* Uncontrolled: the component remembers its own state */}
      <ToggleSwitch defaultChecked />
      <Button disabled={!on} onClick={() => setOn(false)}>
        Reset
      </Button>
    </div>
  );
}

export default App;
```

Don't flip `value` back and forth between a value and `undefined` — that's a mode change, which isn't supported.

## Callback signatures

Value callbacks are uniformly **value-first**:

```ts
type ChangeHandler<T, P extends EventTarget = HTMLElement> = (
  value: T,
  event?: ChangeEvent<P>,
) => void;
```

Most cases only use the first argument (`onChange={setValue}` is enough). The second argument is the native event that triggered the change, and **is only provided by the input components**.

Callbacks that don't follow this signature:

| Component                      | Callback                     | Signature                                            |
| ------------------------------ | ---------------------------- | ---------------------------------------------------- |
| `ToggleSwitch`, `ToggleButton` | `onChange`                   | `(checked: boolean) => void`                         |
| `SelectFileInputWidget`        | `onChange`                   | `(files: File[]) => void`                            |
| `SearchWidget`                 | `onQueryChange` / `onChoose` | The query value / the chosen result                  |
| `Message`                      | `onClose`                    | `() => void`, fired when the close button is clicked |

"Choose" callbacks that are easy to confuse:

- The `Select` family offers both `onChange` (the selected value changed; re-selecting the same item doesn't fire) and `onChoose` (fires on every choice, including repeats). When `clearOnChoose` is true, only `onChoose` fires and the selected value doesn't change — handy for command menus.
- `SearchWidget` has no `onChange`; query changes go through `onQueryChange`.

## Dialogs must be controlled

`Dialog`
, `MessageDialog`
 and `ProcessDialog`
 only have `open`
 (required), with no `defaultOpen`
: opening and closing are driven by your state, and close actions notify you through callbacks. 
| Callback          | When it fires                                                        |
| ----------------- | -------------------------------------------------------------------- |
| `onEscape`        | ESC is pressed (when `escapable` is true)                            |
| `onPrimaryAction` | Ctrl / Cmd + Enter is pressed                                        |
| `onReady`         | The opening animation ends, so you can perform actions like focusing |

`MessageDialog` also has `onOk` / `onCancel`, and `ProcessDialog` also has `onAction`; see their component pages.

## When the controlled value isn't among the options

The `value` you pass in sometimes matches no entry in `options`, for example when an option is removed, the initial value is wrong, or the options load asynchronously and aren't there yet on the first render. How components respond:

| Components                                                                         | When no option matches                                                               | Write back via `onChange`?                                                                     |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| `IndexLayout`, `BookletLayout`, `StackLayout`, `DropdownInput`, `RadioSelectInput` | Automatically show a fallback item (usually the first selectable one), never a blank | Yes: they hand the fallback value back through `onChange` so your state catches up with the UI |
| `Select`, `Dropdown`, `TagMultiselect`, `MenuTagMultiselect`                       | Select nothing and render your value as-is                                           | No                                                                                             |

For the first group, suppose `options` are `a` and `b` but you pass `value="c"`:

```tsx
const options = [
  { value: "a", label: "A", children: "Panel A" },
  { value: "b", label: "B", children: "Panel B" },
];

// `c` is not among the options
<IndexLayout value="c" onChange={setTab} options={options} />
```

The component shows `a`
 (the fallback) and calls `onChange("a")`
 to tell you "this is what actually took effect". Update your state to `a`
 and `value`
 and the UI line up again. The same invalid value is written back only once, so ignoring the callback won't cause it to fire repeatedly. 
The second group follows your `value` exactly: no match means nothing is selected, and the UI shows that the current value has no match; whether to correct it is up to you.
