` and the input element is an `
` wrapped in an `oo-ui-numberInputWidget-field` container (with buttons on, the root also carries `oo-ui-numberInputWidget-buttoned`). The placement rules are the same as [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md):
- `...rest` from props goes on the root div; attributes that must go on the native `
` (such as `role`, `aria-*`, `autoComplete`) go through the `inputProps` channel, whose `onChange`/`onBlur`/`onFocus` are chained after the component's own logic.
- `inputRef` points to the inner `
` (the component `ref` points to the root div); use `inputRef` to focus the input.
- `tabIndex`, `title`, `dir`, `accessKey` and `name` are taken over by the component and written directly to the `
` (matching the original's placement), not via `rest`.
## API
| Prop | Description | Type | Default |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------- |
| `value` | Input value (controlled; passing it enables controlled mode); `''` means no value | `number \| ''` | — |
| `defaultValue` | Uncontrolled initial value | `number \| ''` | — |
| `onChange` | Value-change handler (value-first, includes the native event) | `ChangeHandler
` | — |
| `showButtons` | Whether to show the stepping buttons on both sides | `boolean` | `true` |
| `min` / `max` | Minimum / maximum (stepping clamp + soft-validation bounds) | `number` | — |
| `step` | Validity step (the value must be a multiple); decimals unrestricted by default | `number` | — |
| `buttonStep` | Step for buttons / ↑↓ / wheel | `number` | `step ?? 1` |
| `pageStep` | Step for PgUp/PgDn | `number` | `buttonStep × 10` |
| `allowInteger` / `isInteger` | Deprecated compatibility options: integers only (forces `step={1}`); a development warning fires when set | `boolean` | — |
| `placeholder` | Input hint | `string` | — |
| `label` / `invisibleLabel` | Field label / label visually hidden (kept as accessible name) | `ReactNode` / `boolean` | — / `false` |
| `labelPosition` | Label position | `'before' \| 'after'` | `'after'` |
| `icon` | Icon name | `string` | — |
| `indicator` | Indicator (falls back to required when `required` and not given explicitly) | `'up' \| 'down' \| 'clear' \| 'required'` | — |
| `required` | Required (native `required` attribute; empty is invalid, part of browser validation) | `boolean` | `false` |
| `readOnly` | Read-only (keeps focus; forbids edits and stepping) | `boolean` | `false` |
| `disabled` | Whether disabled | `boolean` | `false` |
| `flags` | Extra flags on the root (the invalid flag is layered on top) | `string \| string[]` | — |
| `name` | Form field name (written to ` `) | `string` | — |
| `accessKey` | Access key (written to ` `) | `string` | — |
| `inputRef` | Ref to the inner ` ` | `Ref` | — |
| `inputProps` | Extra-props channel for the native ` `; `onChange`/`onBlur`/`onFocus` are chained after the component logic | `object` | — |
| `...rest` | Native `div` props (`className`, `id`, `data-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
## Differences from OOUI
- Non-positive `step` / `buttonStep` / `pageStep` throws at construction time in the original; here it logs a one-time development warning and renders with the given values.{/* deviations: dev-numberinput-step */}
- `allowInteger` / `isInteger` are kept for compatibility: turning them on is equivalent to forcing `step={1}`, with a development warning suggesting migration (the original adopts them silently) — new code should use `step` directly.{/* deviations: dev-allow-integer */}
## See also
- The single-line foundation (type whitelist, `validate` soft validation, etc.): [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md)
- How values flow between the component and your state: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md)
- The pass-through and `ref` rules shared by all components: [Common props](https://ooui-react.pages.dev/en/guide/basics.md)
---
url: https://ooui-react.pages.dev/en/components/search-input/index.md
---
> 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.
# SearchInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/SearchInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.SearchInputWidget.html "OO.ui.SearchInputWidget")
A search input: the search form of [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md), with a clear indicator appearing when the value is non-empty and editable — click it to clear and refocus the input.
## Basic usage
```tsx preview
import { useState } from "react";
import { SearchInput } from "ooui-react";
function App() {
const [query, setQuery] = useState("");
return (
Query: {query || "(empty)"}
);
}
export default App;
```
When the field holds a value and is editable (not disabled, not read-only), a × clear indicator shows on the right; clearing the value removes it again and returns focus to the input.
## With labels and soft validation
`label`, `labelPosition`, `invisibleLabel`, `validate` and the rest follow TextInput's semantics.
```tsx preview
import { SearchInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
:::note Want an input with a persistent results list?
This component is just a search box with clear capability. To show dropdown results live as you type, use `SearchWidget` (a search box combined with a results list).
:::
## Attribute placement
Same as TextInput: `...rest` goes on the root ``, props for the native `
` go through `inputProps`, and `inputRef` focuses the input. The clear indicator is taken over by the component, so `type` (always `search`), `indicator` and `indicatorProps` are **not exposed**.
## API
Only the differences from TextInput are listed; the rest are in [TextInput · API](https://ooui-react.pages.dev/en/components/text-input/index.md#api).
| Prop | Description | Type | Default |
| -------------- | ----------------------------------- | ----------------------------------------- | ---------- |
| `value` | Query text (controlled) | `string` | — |
| `defaultValue` | Initial value for uncontrolled use | `string` | — |
| `onChange` | Value-change callback (value-first) | `ChangeHandler
` | — |
| `icon` | Left icon name | `string` | `'search'` |
| `inputRef` | Ref to the inner ` ` | `Ref` | — |
Unsupported props: `type` (always `search`), `indicator` / `indicatorProps` (the indicator slot is taken over by the clear logic).
## See also
- The base single-line input's full capabilities (types, soft validation, prop placement): [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md)
- Search with a results list: `SearchWidget`
- How values flow between the component and your state: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md)
---
url: https://ooui-react.pages.dev/en/components/search-widget/index.md
---
> 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.
# SearchWidget
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/SearchWidget) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.SearchWidget.html "OO.ui.SearchWidget")
A search widget: a query box with an always-visible results list.
## Basic usage
The component doesn't set a height on its own: query and results are absolutely positioned and the root element carries no height. It's recommended to give the root element a height (a fixed height, `height: 100%`, or `flex: 1` in a flex column all work), otherwise the results list overflows the widget box. The demo also adds `position: relative` to the root so those regions anchor to the widget itself (by default they anchor to the nearest positioned ancestor).
```tsx preview
import { useState } from "react";
import { SearchWidget } from "ooui-react";
const data = ["Apple", "Banana", "Orange", "Grape", "Melon"];
function App() {
const [query, setQuery] = useState("");
const [picked, setPicked] = useState("(nothing chosen)");
// The caller filters and fills results from the query
const results = data
.filter((name) => name.toLowerCase().includes(query.toLowerCase()))
.map((name) => ({ value: name, children: name }));
return (
setPicked(String(v))}
results={results}
placeholder="Search fruit…"
style={{ height: 280, position: "relative" }}
/>
Chosen: {picked}
);
}
export default App;
```
## Query and choosing
- `value` / `defaultValue` / `onQueryChange` are the query box's controlled/uncontrolled channels (there is no `onChange`; query changes go through `onQueryChange`).
- Each `results` item is standard [option data](https://ooui-react.pages.dev/en/guide/options.md) (with `value` and `children`).
- `onChoose` fires when a result is chosen (Enter on the highlighted result, or clicking one). The highlight clears whenever the results set or query changes.
- The component implements no searching; real use is mostly async: fire the request in `onQueryChange`, optionally empty `results` while loading and fill them in when ready — the highlight clears automatically on every results/query change, no manual reset needed.
- `inputProps` is a **two-layer channel**: SearchInput-level props are passed directly; props meant for the native ` ` (`aria-*`, `autoComplete`, etc.) go through `inputProps.inputProps` (TextInput's input-element channel). `inputProps.inputRef` is merged — that input doubles as the focus owner for the results list's `aria-activedescendant`.
## Keyboard and focus
Focus always stays in the query box: ↑↓ move the highlight among results (wrapping at the ends), Enter chooses the current highlight. The clear indicator on the query box comes from [SearchInput](https://ooui-react.pages.dev/en/components/search-input/index.md) — it appears when there's a value and clears on click.
## API
| Prop | Description | Type | Default |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- |
| `results` | Result option set, filled by the caller from the query | `SelectOptionProps[]` | `[]` |
| `value` | Query text (controlled) | `string` | — |
| `defaultValue` | Initial query for uncontrolled use | `string` | `''` |
| `onQueryChange` | Query-change callback (refill `results` from it) | `ChangeHandler` | — |
| `onChoose` | Result-chosen callback | `ChangeHandler` | — |
| `placeholder` | Query box placeholder | `string` | — |
| `inputProps` | Overrides for the query box (SearchInput); a two-layer channel, see [Query and choosing](#query-and-choosing); `value`/`defaultValue`/`onChange` are taken over | `object` | — |
| `disabled` | Whether disabled | `boolean` | `false` |
| `...rest` | Native `div` props passed straight to the root | `HTMLAttributes` | — |
## See also
- Lookup with candidates, shown as tags: [MenuTagMultiselect](https://ooui-react.pages.dev/en/components/menu-tag-multiselect/index.md)
- A free-input dropdown of candidates: [ComboBoxInput](https://ooui-react.pages.dev/en/components/combo-box-input/index.md)
- The query box itself: [SearchInput](https://ooui-react.pages.dev/en/components/search-input/index.md)
---
url: https://ooui-react.pages.dev/en/components/combo-box-input/index.md
---
> 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.
# ComboBoxInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/ComboBoxInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.ComboBoxInputWidget.html "OO.ui.ComboBoxInputWidget")
A combo box: a freely editable input with a dropdown candidate menu; the text doesn't have to be one of the candidates.
## Basic usage
```tsx preview
import { useState } from "react";
import { ComboBoxInput } from "ooui-react";
const options = [
{ value: "red", children: "Red" },
{ value: "green", children: "Green" },
{ value: "blue", children: "Blue" },
];
function App() {
const [text, setText] = useState("");
return (
Current text: {text || "(empty)"}
);
}
export default App;
```
## Labels, icons and soft validation
`label` / `labelPosition` / `invisibleLabel`, `icon` / `indicator`, `validate`, `required` and the rest follow [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md); `validate` is soft feedback (marks `aria-invalid`, never rewrites the value).
```tsx preview
import { ComboBoxInput } from "ooui-react";
function App() {
return (
v.length >= 2}
placeholder="At least two characters"
options={[{ value: "ab", children: "AB" }, { value: "cd", children: "CD" }]}
/>
);
}
export default App;
```
## Keyboard and choosing
Typing only **opens the menu**
and re-anchors the highlight to the option exactly matching the input (no match clears the highlight); the candidate list itself is not filtered by the input — to "filter as you type", narrow `options`
yourself. Clicking the input also re-opens the menu. With `readOnly`
the component is fully read-only: none of the open paths (arrow keys, the dropdown button, clicking the input) can expand the menu (the original would still expand a fully disabled menu on click; here read-only means no expansion).
Choosing (clicking an option, Enter, the dropdown button) writes the option text into the input and the value is always a string: **options with a numeric `value` never show as selected** (matching the original, which compares serialized text — `1` and `'1'` are not equal); use string values when you need the selected state.
| Key | Behavior |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ↑ / ↓ | Open the menu and move the highlight (starting from the first / last option when closed) |
| Enter | Choose the highlighted option (with no highlight, the option exactly matching the input) and close; while the menu is open the default is prevented, so Enter inside a [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md) won't submit the form as a side effect |
| Tab | Taken only while the menu is open: with an unchosen highlight, it commits that option and keeps focus; otherwise it closes the menu and lets focus move on |
| PageUp / PageDown | Page by ±10 (only while the menu is open) |
| Home / End / ← / → | Not taken; the caret stays with the input |
The highlight does not wrap at the ends (unlike `SearchWidget`'s wrap-around); Escape or clicking outside closes the menu (handled in the capture phase, so nested in a [Dialog](https://ooui-react.pages.dev/en/components/dialog/index.md) it won't close the dialog by mistake). Focus stays on the input and the highlighted option is announced via `aria-activedescendant`; the common model is in [Selection and options](https://ooui-react.pages.dev/en/guide/options.md).
## Attribute placement
Same as [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md): `...rest` goes on the root ``, props for the native `
` go through `inputProps`, `inputRef` focuses the input; `tabIndex`, `title`, `dir`, `accessKey` and `name` are taken over and go on the `
`.
## API
Only ComboBoxInput-specific props are listed; input props are in [TextInput · API](https://ooui-react.pages.dev/en/components/text-input/index.md#api).
| Prop | Description | Type | Default |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------------------- |
| `options` | Candidate option set (**required**); see [Selection and options](https://ooui-react.pages.dev/en/guide/options.md) | `DropdownOptionProps[]` | — |
| `value` | Input text (controlled) | `string` | — |
| `defaultValue` | Initial value for uncontrolled use | `string` | `''` |
| `onChange` | Value-change callback (typed and chosen items both fire) | `ChangeHandler
` | — |
| `placeholder` / `name` / `maxLength` / `required` / `readOnly` | Ordinary input props | — | — |
| `label` / `labelPosition` / `invisibleLabel` | Field label / position / visually hidden | — | — / `'after'` / `false` |
| `icon` / `indicator` | Icon name / indicator | `string` / `'up' \| 'down' \| 'clear' \| 'required'` | — |
| `flags` | Extra root flags (invalid overlays it) | `string \| string[]` | — |
| `validate` | Soft validation, see [Soft validation](https://ooui-react.pages.dev/en/components/text-input/index.md#soft-validation) | `RegExp \| ((v: string) => boolean \| Promise) \| 'non-empty' \| 'integer'` | — |
| `inputRef` | Ref to the inner ` ` | `Ref` | — |
| `inputProps` | Extra props for the native ` `; events chain after the component's logic | `object` | — |
| `disabled` | Whether disabled | `boolean` | `false` |
## See also
- The base single-line input: [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md)
- Pick only from candidates (no free entry): [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md)
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
---
url: https://ooui-react.pages.dev/en/components/checkbox-input/index.md
---
> 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.
# CheckboxInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/CheckboxInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.CheckboxInputWidget.html "OO.ui.CheckboxInputWidget")
A single checkbox. It has no text label of its own — when you need one, wrap it in [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md).
## Basic usage
The checked state uses the `checked` / `defaultChecked` / `onChange` channels; the first argument of `onChange` is the new checked boolean.
```tsx preview
import { useState } from "react";
import { CheckboxInput } from "ooui-react";
function App() {
const [checked, setChecked] = useState(false);
return (
Checked: {String(checked)}
Uncontrolled; initially checked
);
}
export default App;
```
## Indeterminate state
`indeterminate` means "partially selected" — visually a dash rather than a check. It is independent of the checked state and is commonly used on a "select all" box to show that some, not all, children are selected.
```tsx preview
import { CheckboxInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
The standard wiring of a "select all" box: its checked and indeterminate states are both **derived** from the children set (none → unchecked, all → checked, otherwise indeterminate), and clicking it sets all children to checked or unchecked:
```tsx preview
import { useState } from "react";
import { CheckboxInput } from "ooui-react";
const ITEMS = ["Apple", "Banana", "Orange"];
function App() {
const [picked, setPicked] = useState(["Apple"]);
const allChecked = picked.length === ITEMS.length;
return (
0 && !allChecked}
onChange={() => setPicked(allChecked ? [] : ITEMS)}
/>
Select all
{ITEMS.map((item) => (
setPicked((prev) =>
prev.includes(item) ? prev.filter((x) => x !== item) : [...prev, item],
)
}
/>
{item}
))}
);
}
export default App;
```
## Form submission value
`value` is the **form submission value** (written to the native ` `'s `value` attribute, defaulting to an empty string). It **does not affect the checked state** — that is driven only by the `checked` channels. Combined with `name`, this `value` is submitted with the form when checked.
```tsx preview
import { CheckboxInput } from "ooui-react";
function App() {
return (
I agree to the terms (submits as agree=yes)
);
}
export default App;
```
## Using it with FieldLayout
The field's text label is usually delegated to [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md). It automatically associates the label with this checkbox's native ` ` (clicking the label toggles the check); to specify the association target manually, give the input element's id via `inputId`.
```tsx preview
import { CheckboxInput, FieldLayout } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Disabled
`disabled` outputs `aria-disabled` and disables the native ` `, so the checkbox can no longer be toggled. Inside a native `` such as `FieldsetLayout`, the group-level disable propagates to it.
```tsx preview
import { CheckboxInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| ---------------- | ------------------------------------------------------------------------------------- | ------------------------------------------ | ------- |
| `checked` | Checked state (controlled; passing it enables controlled mode) | `boolean` | — |
| `defaultChecked` | Uncontrolled initial checked state | `boolean` | — |
| `onChange` | Checked-change handler (new state first, includes the native event) | `ChangeHandler` | — |
| `value` | Form submission value (written to ` `'s `value`, does not affect checked state) | `string \| number` | `''` |
| `indeterminate` | Half-selected state | `boolean` | `false` |
| `required` | Required (native `required`) | `boolean` | `false` |
| `name` | Form field name (goes on ` `) | `string` | — |
| `inputId` | The inner ` `'s id (pairs with a label's `htmlFor`) | `string` | — |
| `inputRef` | Ref to the inner ` ` (the component `ref` points to the outer ``) | `Ref` | — |
| `disabled` | Whether disabled | `boolean` | `false` |
| `accessKey` | Access key (goes on ` `) | `string` | — |
| `...rest` | Native `span` props (`className`, `id`, `data-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
`title`, `dir` and `tabIndex` are taken over by the component and go on the inner ` `; `role` goes there too (semantics like `role="presentation"` must hit the real input to work).
## See also
- Where the checked channel sits among the three value channels: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md)
- Layout for a labeled field: [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md)
- The single-choice case: [RadioInput](https://ooui-react.pages.dev/en/components/radio-input/index.md) (same props as this component, without `indeterminate`)
---
url: https://ooui-react.pages.dev/en/components/radio-input/index.md
---
> 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.
# RadioInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/RadioInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.RadioInputWidget.html "OO.ui.RadioInputWidget")
A single radio button. It has no text label of its own — when you need one, wrap it in [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md).
## Basic usage
The checked state uses the `checked` / `defaultChecked` / `onChange` channels; the first argument of `onChange` is the new checked boolean. Note that a radio cannot be un-checked by clicking it again — clearing an exclusive group needs an extra mechanism (such as a "clear" button that resets the state).
```tsx preview
import { useState } from "react";
import { Button, RadioInput } from "ooui-react";
function App() {
const [checked, setChecked] = useState(true);
return (
Checked: {String(checked)}
Uncontrolled; initially checked
setChecked(false)}>Clear
);
}
export default App;
```
## Same-name exclusivity and the form submission value
Give several `RadioInput`s the same `name` and the browser's native mechanism guarantees group exclusivity with at most one item submitted with the form. `value` is the **form submission value** (written to the native ` `'s `value` attribute, defaulting to an empty string). It **does not affect the checked state**.
```tsx preview
import { useState } from "react";
import { RadioInput } from "ooui-react";
function App() {
const [size, setSize] = useState("medium");
return (
{["small", "medium", "large"].map((item) => (
next && setSize(item)}
/>
{item}
))}
Current selection: {size}
);
}
export default App;
```
## Using it with FieldLayout
The field's text label is usually delegated to [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md). It automatically associates the label with this radio's native ` ` (clicking the label selects it); to specify the association target manually, give the input element's id via `inputId`.
```tsx preview
import { FieldLayout, RadioInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Disabled
`disabled` outputs `aria-disabled` and disables the native ` `, so the radio can no longer be selected. Inside a native `` such as `FieldsetLayout`, the group-level disable propagates to it.
```tsx preview
import { RadioInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| ---------------- | ------------------------------------------------------------------------------------- | ------------------------------------------ | ------- |
| `checked` | Checked state (controlled; passing it enables controlled mode) | `boolean` | — |
| `defaultChecked` | Uncontrolled initial checked state | `boolean` | — |
| `onChange` | Checked-change handler (new state first, includes the native event) | `ChangeHandler` | — |
| `value` | Form submission value (written to ` `'s `value`, does not affect checked state) | `string \| number` | `''` |
| `name` | Form field name (written to ` `; same name = same exclusive group) | `string` | — |
| `inputId` | The inner ` `'s id (pairs with a label's `htmlFor`) | `string` | — |
| `required` | Required (native `required`) | `boolean` | `false` |
| `disabled` | Whether disabled | `boolean` | `false` |
| `accessKey` | Access key (written to ` `) | `string` | — |
| `...rest` | Native `span` props (`className`, `id`, `data-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
`title`, `dir` and `tabIndex` are taken over by the component and written to the inner ` `; `role` is written there too (semantics like `role="presentation"` must hit the real input to work). The component `ref` points to the outer ``.
## See also
- Where the checked channel sits among the value channels: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md)
- Layout for a labeled field: [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md)
- The radio group (exclusive items in one group, arrow keys select directly): [RadioSelect](https://ooui-react.pages.dev/en/components/radio-select/index.md)
---
url: https://ooui-react.pages.dev/en/components/select-file-input-widget/index.md
---
> 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.
# SelectFileInputWidget
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/SelectFileInputWidget) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.SelectFileInputWidget.html "OO.ui.SelectFileInputWidget")
A file-picker input, with drop-zone and button-only forms.
## Basic usage
The file set is an **array** driven by the `value` / `defaultValue` / `onChange` channels; when not multiple, only the first file is kept. The **clear indicator in the info field is the only way to clear** (click it or press Enter).
```tsx preview
import { useState } from "react";
import { SelectFileInputWidget } from "ooui-react";
function App() {
const [files, setFiles] = useState([]);
return (
{files.length ? `Selected: ${files.map((file) => file.name).join(", ")}` : "No file selected"}
);
}
export default App;
```
## Multiple selection and type filtering
`multiple` enables multi-select; `accept` restricts types with MIME or `image/*` patterns — it is written to the `accept` attribute and filters both the picker results and dropped files (the picker is already restricted, so most filtering happens on the drop path; files without a type are let through).
```tsx preview
import { SelectFileInputWidget } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Drop-zone form
`showDropTarget` turns the whole component into a drop zone: when empty the whole block opens the picker on click, dragged-in files get accepted/rejected feedback, and dropping selects them. In single-select mode with an image file, a **thumbnail** is shown — loaded only for images within `thumbnailSizeLimit` (MB, default 20), falling back to an attachment icon when oversized or undecodable.
Dragging relies on the browser's `DataTransfer` constructor (Safari 14.1+); when unavailable, dragging turns off automatically and the select button keeps working. Pass `droppable={false}` to switch dragging off explicitly.
```tsx preview
import { useMemo } from "react";
import { SelectFileInputWidget } from "ooui-react";
const PNG_1PX =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
function App() {
// A 1x1 PNG: with an initial image file, the single-select drop zone loads a thumbnail
const imageFile = useMemo(() => {
const bytes = Uint8Array.from(atob(PNG_1PX), (char) => char.charCodeAt(0));
return new File([bytes], "logo.png", { type: "image/png" });
}, []);
return (
{/* Limit set to 0: no thumbnail, falls back to the attachment icon */}
);
}
export default App;
```
## Button-only form
`buttonOnly` renders just the select button, without the info field; use it when you display the file name yourself.
```tsx preview
import { useState } from "react";
import { SelectFileInputWidget } from "ooui-react";
function App() {
const [name, setName] = useState("");
return (
setName(files[0]?.name ?? "")}
/>
{name || "No file selected"}
);
}
export default App;
```
## Controlled use and programmatic changes
Passing `value` enables controlled mode — the file set is fully caller-driven. The demo wires an external "Clear" button that resets the set to `[]`.
:::note Differences from OOUI
A `value`
passed at construction is silently dropped by the original (a construction-order defect) and can only be applied afterwards with an imperative `setValue`
. Following the controlled convention, this implementation applies `value`
/ `defaultValue`
directly and writes the set back to the inner ` `
, so native form submission keeps working.
:::
```tsx preview
import { useState } from "react";
import { Button, SelectFileInputWidget } from "ooui-react";
function App() {
const [files, setFiles] = useState([]);
return (
setFiles([])} disabled={files.length === 0}>
Clear
);
}
export default App;
```
## Focus and keyboard
- The **select button** is the `Tab` stop; the info field is out of the Tab order. The clear indicator is keyboard-reachable (focus it and press Enter to clear).
- The info field is a read-only `type="search"` input — typing in it never changes the file set; clearing always goes through the clear indicator.
- `required` and `name` are written to the inner file ` ` and submit natively with [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md).
## API
| Prop | Description | Type | Default |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------- |
| `value` | Current file set (controlled; passing it enables controlled mode; empty array = no selection), first file only when not multiple | `File[]` | — |
| `defaultValue` | Initial file set for uncontrolled use | `File[]` | `[]` |
| `onChange` | File-set change callback; not fired when the new set is equivalent to the old one (same name, size, type and last-modified) | `(files: File[]) => void` | — |
| `accept` | Accepted file types (MIME or `image/*` patterns), written to the `accept` attribute and filtering both picker results and drops | `string[]` | — |
| `multiple` | Whether multiple files can be selected | `boolean` | `false` |
| `droppable` | Whether dropping is allowed (forced off when the browser lacks `DataTransfer`) | `boolean` | `true` |
| `showDropTarget` | Drop-zone form: the whole block is droppable and clickable (requires `droppable`) | `boolean` | `false` |
| `buttonOnly` | Render only the select button (takes priority over `showDropTarget`) | `boolean` | `false` |
| `thumbnailSizeLimit` | Thumbnail size limit in MB (no thumbnail beyond it) | `number` | `20` |
| `placeholder` | Info-field placeholder (defaults to the built-in message) | `string` | — |
| `icon` | Info-field icon (none by default) | `string` | — |
| `required` | Required (native `required`, on the file ` `) | `boolean` | `false` |
| `name` | File field name (on the file ` `, submitted with the form) | `string` | — |
| `buttonLabel` | Select-button text (defaults to the built-in message depending on `multiple`) | `ReactNode` | — |
| `buttonProps` | Select-button props override (`disabled` / `onClick` are taken over by the component) | `Omit` | — |
| `inputRef` | Ref to the inner file ` ` | `Ref` | — |
| `accessKey` | Access key (written to the file ` `) | `string` | — |
| `disabled` | Whether disabled (button, info field and dragging all off) | `boolean` | `false` |
| `...rest` | Native `div` props (`className`, `id`, `data-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
`title` is written to the file ` ` and `tabIndex` to the select button; the component `ref` points to the root element (the button itself in the `buttonOnly` form).
## See also
- The value-channel convention: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md)
- Replacing built-in messages with a language pack: [Global configuration](https://ooui-react.pages.dev/en/guide/configuration.md#messages-and-i18n)
- The form submission container: [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md)
---
url: https://ooui-react.pages.dev/en/components/select/index.md
---
> 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.
# Select
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/Select) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.SelectWidget.html "OO.ui.SelectWidget")
A selection list: a clickable set of options — the inner list of floating components like [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md), and usable on its own. The option data format is described in [Selection and options](https://ooui-react.pages.dev/en/guide/options.md).
:::note How to interact with it standalone
Matching the original `SelectWidget`
, this component's root is **not focusable by default**
(it is not in the Tab order), so it is best suited to mouse selection. For keyboard-reachable single or multiple selection, prefer components that handle focus for you — [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md)
, [RadioSelect](https://ooui-react.pages.dev/en/components/radio-select/index.md)
, `ButtonSelect`
— all of which use `Select`
as their inner list. As a menu's inner layer, keyboard navigation is handled by the outer trigger for you, so you don't have to manage it.
:::
## Basic usage
```tsx preview
import { useState } from "react";
import { Select } from "ooui-react";
const options = [
{ value: "apple", children: "Apple" },
{ value: "orange", children: "Orange" },
{ value: "banana", children: "Banana", disabled: true },
{ value: "grape", children: "Grape" },
];
function App() {
const [value, setValue] = useState("apple");
return (
Current value: {String(value)}
);
}
export default App;
```
## onChange and onChoose
The two "choose" callbacks have different roles:
- `onChange`: fires when the **selected value changes**; re-clicking the current item doesn't fire it.
- `onChoose`: fires on **every choice** (click / drag / Enter), including repeats. Floating components use it to "close on choose".
```tsx
setOpen(false)} />
```
## Command-menu form
When `clearOnChoose` is true, a choice only fires `onChoose` and doesn't change the selected value — a "selected" state never appears, which suits command menus that run an action and keep no selection.
```tsx preview
import { useState } from "react";
import { Select } from "ooui-react";
function App() {
const [log, setLog] = useState([]);
return (
setLog((prev) => [...prev, `ran ${String(v)}`])}
/>
{log.length ? log.join(" → ") : "Click any command (no selection appears)"}
);
}
export default App;
```
## Group headings
Items in the `options` array without a `value` render as group headings — not selectable and skipped by keyboard navigation.
```tsx preview
import { Select } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Keyboard navigation switches
As a menu's inner layer, keyboard navigation is driven by the outer component. For the standalone case where the list has focus:
- `handleNavigationKeys`: whether Home / End / PageUp / PageDown are handled.
- `listWrapsAround`: whether arrow navigation wraps at the ends (default `true`).
- **Prefix jump**: with the list focused, typing characters cycles through options by text prefix (1.5 s buffer).
- **Hover highlight**: the hovered option becomes the highlighted one; the highlight clears on mouse-out (shared state with keyboard highlighting).
## API
| Prop | Description | Type | Default |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ------- |
| `options` | The option set; see [Selection and options](https://ooui-react.pages.dev/en/guide/options.md) for the convention | `SelectOptionProps[]` | — |
| `value` | Current selected value (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial selected value for uncontrolled use | `string \| number` | — |
| `onChange` | Selected-value change callback (value-first, fires only on change) | `ChangeHandler` | — |
| `onChoose` | Choose callback (fires on every choice, incl. repeats) | `ChangeHandler` | — |
| `clearOnChoose` | Command-menu form: a choice only fires `onChoose` and leaves the value unchanged | `boolean` | `false` |
| `outline` | Render options as hierarchy-indented `OutlineOption`s | `boolean` | `false` |
| `highlightedValue` | Keyboard-highlighted value (controlled; pass it to manage from above — internally maintained when used standalone) | `string \| number` | — |
| `onHighlightedChange` | Highlight-change callback; write back through it when `highlightedValue` is controlled | `ChangeHandler` | — |
| `selectedValues` | A set of selected values for multi-select display (used by tag inputs etc.) | `(string \| number)[]` | — |
| `handleNavigationKeys` | Whether Home/End/PageUp/PageDown are handled | `boolean` | `false` |
| `listWrapsAround` | Whether keyboard navigation wraps at the ends | `boolean` | `true` |
| `disabled` | Whether disabled (also disables every option in the group) | `boolean` | `false` |
| `...rest` | Native `div` props (`className`, `id`, `aria-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
There is no `children` slot; every option is declared through `options`.
## See also
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
- Floating components that use Select as their inner list: [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md)
- The division of labor between `onChange` and `onChoose`: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md#callback-signatures)
---
url: https://ooui-react.pages.dev/en/components/dropdown/index.md
---
> 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.
# Dropdown
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/Dropdown) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.DropdownWidget.html "OO.ui.DropdownWidget")
A dropdown select. The option data format is described in [Selection and options](https://ooui-react.pages.dev/en/guide/options.md).
## Basic usage
When `value` is not passed, the component remembers the selection itself, and `label` is the placeholder text shown until something is selected. `onChange` is value-first.
```tsx preview
import { useState } from "react";
import { Dropdown } from "ooui-react";
const options = [
{ value: "apple", children: "Apple" },
{ value: "orange", children: "Orange" },
{ value: "banana", children: "Banana" },
];
function App() {
const [value, setValue] = useState("apple");
return (
Current value: {String(value)}
);
}
export default App;
```
## Icon and label
`icon` renders at the front of the handle. `label` plays two roles: it is the handle's display text when nothing is selected, and it contributes to the combobox's accessible name.
```tsx preview
import { Dropdown } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Groups and disabled items
Items without a `value` render as group headings; items with `disabled` cannot be selected. For menu-style components, `disabled` disables the **trigger** (the handle won't open, keyboard is off), while each item inside the menu is governed by its own `disabled` — see [Selection and options · Disabled](https://ooui-react.pages.dev/en/guide/options.md#disabled).
```tsx preview
import { Dropdown } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Keyboard and focus
- Tab focuses the handle, Enter / Space opens the menu; when open, ↑↓←→ move the highlight, Enter / Space choose and close, and Escape closes without changing the value.
- While closed but focused, navigation keys and prefix jump **change the selection directly without opening the menu** (screen-reader friendly, matching the original).
- Typing characters jumps by option text prefix (1.5 s buffer).
- Focus always stays on the handle; the highlighted item is linked via `aria-activedescendant`, and the menu itself is not in the Tab order.
:::note Differences from OOUI
With the menu open, **Space**
: the original never feeds Space to the menu — a keypress merely simulates a click that closes it; here Space shares the Enter branch and directly chooses the highlighted item (matching the ARIA convention of activating buttons with Space). While the type-ahead buffer is active, Space still counts as prefix search and does not choose.
:::
## API
| Prop | Description | Type | Default |
| -------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------- | ------- |
| `options` | The option set; see [Selection and options](https://ooui-react.pages.dev/en/guide/options.md) for the format | `DropdownOptionProps[]` | — |
| `value` | Current selected value (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial selected value for uncontrolled use | `string \| number` | — |
| `onChange` | Selection-change callback (fires when the value changes) | `ChangeHandler` | — |
| `label` | Placeholder text when nothing is selected; also part of the accessible name | `ReactNode` | — |
| `icon` | Icon name at the front of the handle | `string` | — |
| `disabled` | Whether the trigger is disabled (won't open, keyboard off) | `boolean` | `false` |
| `tabIndex` | Tab order; goes on the handle (the root is not focusable) | `number \| null` | `0` |
| `...rest` | Native `div` props (`className`, `id`, `data-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
There is no `children` slot — the handle and menu are a fixed structure, and the display text comes from the selected option or `label`.
## See also
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
- The inner list of the menu: [Select](https://ooui-react.pages.dev/en/components/select/index.md)
- How values flow between the component and your state: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md)
---
url: https://ooui-react.pages.dev/en/components/outline-select/index.md
---
> 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.
# OutlineSelect
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/OutlineSelect) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.OutlineSelectWidget.html "OO.ui.OutlineSelectWidget")
An outline select: `level` (0–2) expresses hierarchical indentation, suited to tables of contents and similar hierarchical lists.
## Basic usage
```tsx preview
import { useState } from "react";
import { OutlineSelect } from "ooui-react";
function App() {
const [value, setValue] = useState("intro");
return (
Current item: {String(value)}
);
}
export default App;
```
## Levels and groups
- `level` is 0–2 (out-of-range is clamped); the theme only provides three indentation levels.
- Options without a `value` render as group headings, following the conventions in [Selection and options](https://ooui-react.pages.dev/en/guide/options.md).
- Options with icon slots support `icon` and `flags` (tinting the icon, see [Selection and options · Icons and flags](https://ooui-react.pages.dev/en/guide/options.md#icons-and-flags)).
- Keyboard: with the group focused, ↑↓←→ move the highlight and Enter chooses; Home/End/PageUp/PageDown are enabled via `handleNavigationKeys`, and `listWrapsAround` controls wrapping — exactly as in [Select](https://ooui-react.pages.dev/en/components/select/index.md).
```tsx preview
import { useState } from "react";
import { OutlineSelect } from "ooui-react";
function App() {
const [value, setValue] = useState("getting-started");
return (
);
}
export default App;
```
## API
Inherits [Select](https://ooui-react.pages.dev/en/components/select/index.md) props (`value` / `defaultValue` / `onChange` / `disabled` / `handleNavigationKeys` / `listWrapsAround`, etc.); the differences are:
| Prop | Description | Type | Default |
| ---------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------- |
| `options` | Outline option set (**required**); selectable items also support `level` (0–2 indentation) and `icon` / `flags` | `OutlineSelectOptionProps[]` | — |
| `tabIndex` | Tab order, on the whole group (the outline list is focusable) | `number \| null` | `0` |
`outline` is fixed on by the component and cannot be overridden.
## See also
- The base selection list: [Select](https://ooui-react.pages.dev/en/components/select/index.md)
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
---
url: https://ooui-react.pages.dev/en/components/tab-select/index.md
---
> 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.
# TabSelect
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/TabSelect) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.TabSelectWidget.html "OO.ui.TabSelectWidget")
A tab select: a set of mutually exclusive options shown as a row of tabs.
## Basic usage
```tsx preview
import { useState } from "react";
import { TabSelect } from "ooui-react";
const options = [
{ value: "general", children: "General" },
{ value: "advanced", children: "Advanced" },
{ value: "about", children: "About" },
];
function App() {
const [tab, setTab] = useState("general");
return (
Current tab: {String(tab)}
);
}
export default App;
```
## Framed and frameless
`framed` (on by default) gives the tab strip a bordered background; set it to `false` for a lighter frameless form.
```tsx preview
import { TabSelect } from "ooui-react";
const options = [
{ value: "a", children: "One" },
{ value: "b", children: "Two" },
{ value: "c", children: "Three", disabled: true },
];
function App() {
return (
);
}
export default App;
```
## Keyboard
After a click, focus moves into the group automatically (slightly better than the original, which leaves focus where it was so the arrow keys stay dead), and ←→ (and ↑↓) then **cycle among non-disabled tabs and select directly**; Enter re-affirms the current tab. Pressing Tab also moves focus into the group. This is the direct-select family model; see [Selection and options · Keyboard and focus](https://ooui-react.pages.dev/en/guide/options.md#keyboard-and-focus).
## API
| Prop | Description | Type | Default |
| -------------- | ------------------------------------------------------------------------------------------ | --------------------------------- | ------- |
| `options` | Tab set (**required**); each item has `value` and `children` (text), optionally `disabled` | `TabSelectOptionProps[]` | — |
| `value` | Current selected value (controlled) | `string \| number` | — |
| `defaultValue` | Initial selected value for uncontrolled use | `string \| number` | — |
| `onChange` | Selected-value change callback (value-first) | `ChangeHandler` | — |
| `framed` | Whether the strip has a border | `boolean` | `true` |
| `disabled` | Whether disabled (also disables every tab) | `boolean` | `false` |
| `tabIndex` | Tab order (focus rests on the group) | `number \| null` | `0` |
| `...rest` | Native `div` props (`className`, `id`, `aria-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
## Differences from OOUI
- **`TabOption` does not support `href` links**: the original only used that from PHP; compose link tabs yourself if you need them. {/* deviations: dev-taboption-href */}
- **A lost mouseup no longer leaves a stale drag state**: in the original, dragging out of the window and releasing left the selecting state behind, so the next click on empty space submitted the stale option; this library resets on mousedown and cleans up on `pointercancel`. {/* deviations: dev-tabselect-drag-fix */}
## See also
- Button-style direct select: [ButtonSelect](https://ooui-react.pages.dev/en/components/button-select/index.md)
- Multiple panels with tabs: `BookletLayout`
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
---
url: https://ooui-react.pages.dev/en/components/button-select/index.md
---
> 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.
# ButtonSelect
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/ButtonSelect) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.ButtonSelectWidget.html "OO.ui.ButtonSelectWidget")
A button select: a set of mutually exclusive options shown as a row of buttons. See [Selection and options](https://ooui-react.pages.dev/en/guide/options.md) for the option data format.
## Basic usage
```tsx preview
import { useState } from "react";
import { ButtonSelect } from "ooui-react";
const options = [
{ value: "left", icon: "alignLeft", children: "Align left" },
{ value: "center", icon: "alignCenter", children: "Center" },
{ value: "right", icon: "alignRight", children: "Align right" },
];
function App() {
const [align, setAlign] = useState("left");
return (
Current alignment: {String(align)}
);
}
export default App;
```
## Unframed and disabled items
Options support `framed={false}`; items with `disabled` are not selectable, and a group-level `disabled` disables the whole group (see [Selection and options · Disabled](https://ooui-react.pages.dev/en/guide/options.md#disabled)).
```tsx preview
import { ButtonSelect } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Keyboard
After a click, focus moves into the group automatically (slightly better than the original, which leaves focus where it was so the arrow keys stay dead), and ↑↓←→ then **cycle among non-disabled items and select directly**; Enter re-affirms the current item. Pressing Tab also moves focus into the group. This is the "direct-select family" keyboard model (shared by [TabSelect](https://ooui-react.pages.dev/en/components/tab-select/index.md) and [RadioSelect](https://ooui-react.pages.dev/en/components/radio-select/index.md)).
## API
| Prop | Description | Type | Default |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- |
| `options` | Option set (**required**); each item has `value` and `children` (text), optionally `icon` / `indicator` / `flags` / `framed` / `disabled` | `ButtonSelectOptionProps[]` | — |
| `value` | Current selected value (controlled) | `string \| number` | — |
| `defaultValue` | Initial selected value for uncontrolled use | `string \| number` | — |
| `onChange` | Selected-value change callback (value-first) | `ChangeHandler` | — |
| `disabled` | Whether disabled (also disables every option in the group) | `boolean` | `false` |
| `tabIndex` | Tab order (focus rests on the group) | `number \| null` | `0` |
| `...rest` | Native `div` props (`className`, `id`, `aria-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
## Differences from OOUI
- **Initially selected framed options are inverted too**: the original inverts only after a user click while initially selected ones stay normal; this library applies the theme rule uniformly — active or disabled means inverted. {/* deviations: dev-buttonoption-invert */}
## See also
- Tab-style direct select: [TabSelect](https://ooui-react.pages.dev/en/components/tab-select/index.md)
- Radio group (native radio semantics): [RadioSelect](https://ooui-react.pages.dev/en/components/radio-select/index.md)
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
---
url: https://ooui-react.pages.dev/en/components/button-menu-select/index.md
---
> 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.
# ButtonMenuSelectWidget
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/ButtonMenuSelectWidget) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.ButtonMenuSelectWidget.html "OO.ui.ButtonMenuSelectWidget")
A button-triggered command menu: choosing an item runs it and closes the menu. Unlike a [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md) that tracks a "current selection", it suits toolbar-style "click to run a command" cases.
## Basic usage
```tsx preview
import { useState } from "react";
import { ButtonMenuSelectWidget } from "ooui-react";
function App() {
const [log, setLog] = useState("Nothing chosen");
return (
setLog(`ran: ${String(v)}`)}
>
Actions
{log}
);
}
export default App;
```
## Controlled open state and keeping a selection
- `open` / `defaultOpen` / `onOpenChange` control the menu (the button, outside clicks, Escape and choosing all fire `onOpenChange`).
- `clearOnSelect` defaults to `true`: the menu is a pure command menu with no lingering selection; set it to `false` to keep the last chosen item.
- Options without a `value` render as group headings (the "Advanced" item above). See [Selection and options](https://ooui-react.pages.dev/en/guide/options.md) for the option format.
```tsx preview
import { useState } from "react";
import { ButtonMenuSelectWidget } from "ooui-react";
function App() {
const [chosen, setChosen] = useState("share");
return (
setChosen(value)}
options={[
{ value: "share", children: "Share link" },
{ value: "move", children: "Move page" },
{ value: "protect", children: "Protect page" },
]}
>
Share
Last chosen: {String(chosen)} (kept selected in the menu)
);
}
export default App;
```
## Keyboard
Focus is on the button: Enter / Space opens the menu; with it open, ↑↓ move the highlight, Enter / Space choose, and typed characters jump by prefix; Escape or an outside click closes it. Note that, unlike [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md), typing while the menu is closed and focused does not change the selection directly.
Two differences from OOUI:
- Opening with the keyboard also shows the button's pressed state — in the original, keyboard opening gets its pressed class wiped by the reset flow.
- While the type-ahead buffer is active, Space continues the search and never toggles the menu; with the menu open, Space shares the Enter branch and directly chooses the highlighted item (matching the ARIA convention of activating buttons with Space).{/* deviations: dev-bmsw-pressed dev-bmsw-gesture-guard dev-menu-space-select */}
## API
Inherits [Button](https://ooui-react.pages.dev/en/components/button/index.md) props (`icon` / `indicator` / `flags` / `framed` / `disabled`, etc.; `onClick` fires "before toggling the open state"), and adds:
| Prop | Description | Type | Default |
| --------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ------- |
| `options` | Menu option set (**required**); see [Selection and options](https://ooui-react.pages.dev/en/guide/options.md) | `ButtonMenuSelectWidgetOptionProps[]` | — |
| `open` | Whether the menu is open (controlled) | `boolean` | — |
| `defaultOpen` | Initial open state for uncontrolled use | `boolean` | `false` |
| `onOpenChange` | Menu open-state callback | `(open: boolean) => void` | — |
| `onChoose` | Choose callback (fires on every choice, incl. repeats) | `ChangeHandler` | — |
| `clearOnSelect` | Whether to clear the menu's selection after choosing | `boolean` | `true` |
| `menuSpacing` | Gap between menu and button (px) | `number` | `4` |
| `menuProps` | Menu prop overrides (`open`/`container`/`options`/`onChoose` etc. are taken over) | `object` | — |
## See also
- A dropdown that records the current value: [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md)
- A general button that opens a popup: [PopupButton](https://ooui-react.pages.dev/en/components/popup-button/index.md)
- More command-menu forms: the toolbar's `MenuToolGroup`
---
url: https://ooui-react.pages.dev/en/components/checkbox-multiselect/index.md
---
> 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.
# CheckboxMultiselect
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/CheckboxMultiselect) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.CheckboxMultiselectWidget.html "OO.ui.CheckboxMultiselectWidget")
A checkbox group: several checkbox items in one group, multiple selections allowed.
## Basic usage
The selected value is an **array**, passed through `value` / `defaultValue` / `onChange`; `onChange` carries the full value array. With `name`, every option's checkbox shares it and the checked ones submit as multiple same-name fields.
```tsx preview
import { useState } from "react";
import { CheckboxMultiselect } from "ooui-react";
const options = [
{ value: "apple", children: "Apple" },
{ value: "banana", children: "Banana" },
{ value: "cherry", children: "Cherry" },
];
function App() {
const [value, setValue] = useState>(["banana"]);
return (
Current selection: {value.join(", ") || "(none)"}
);
}
export default App;
```
## Shift+click range selection
Click one item, then click another with `Shift` held: the range between the two clicks (both ends included) is set to the state the **second** item just flipped to; disabled items in the range keep their state. This mirrors bulk-checking in list tables.
```tsx preview
import { useState } from "react";
import { CheckboxMultiselect } from "ooui-react";
const options = [1, 2, 3, 4, 5].map((number) => ({
value: number,
children: `Item ${number}`,
}));
function App() {
const [value, setValue] = useState>([]);
return (
Current selection: {value.join(", ") || "(none)"}
Click one item, then Shift+click another to see range selection
);
}
export default App;
```
## Using it with FieldLayout
To give the whole group a field label, wrap it with [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md). Clicking the field label moves focus to the first enabled option's checkbox (it does not toggle anything — a group has no "one value for the whole group" semantics like a single checkbox does).
```tsx preview
import { CheckboxMultiselect, FieldLayout } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Disabled
- An option's own `disabled` skips that item (not checkable).
- Component-level `disabled` disables every option in the group, and the group state wins. See [Selection and options · Disabled](https://ooui-react.pages.dev/en/guide/options.md#disabled).
```tsx preview
import { CheckboxMultiselect } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Keyboard and focus
- Every option's native ` ` is in the Tab order; ↑↓←→ are an in-group shortcut — they move focus **cyclically between non-disabled items** (unlike `RadioSelect`, they only move focus and **do not select directly**), and Space toggles the current item.
- A Shift+click range starts from the item last clicked (or last toggled with Space).
- `onChange`'s second argument is the native `change` event that triggered this change.
- Option roots carry `role='checkbox'` and `aria-checked` (the original's option roots have no role and express selection only via a class), so screen readers can read each option's state directly. {/* deviations: dev-multioption-role */}
## API
| Prop | Description | Type | Default |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | ------- |
| `options` | The option set, rendered in display order; each item has `value` and `children` (the option text) and supports `disabled`, plus `inputRef` (ref to the inner checkbox), `checkboxProps` (passed to the inner checkbox, e.g. `inputId`) and a per-option `onChange` | `CheckboxMultiselectOptionProps[]` | — |
| `value` | Current selected values (controlled; passing it enables controlled mode) | `(string \| number)[]` | — |
| `defaultValue` | Initial selected values for uncontrolled use | `(string \| number)[]` | `[]` |
| `onChange` | Selected-values change callback with the full value array and the native event | `ChangeHandler<(string \| number)[], HTMLInputElement>` | — |
| `name` | Form field name, passed to every option's `checkbox` | `string` | — |
| `disabled` | Whether disabled (also disables every option in the group) | `boolean` | `false` |
| `...rest` | Native `div` props (`className`, `id`, `aria-*`, etc.) passed straight to the group container | `HTMLAttributes` | — |
## See also
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
- A single checkbox: [CheckboxInput](https://ooui-react.pages.dev/en/components/checkbox-input/index.md)
- The checkbox group field that submits with a form: `CheckboxMultiselectInput`
---
url: https://ooui-react.pages.dev/en/components/radio-select/index.md
---
> 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.
# RadioSelect
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/RadioSelect) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.RadioSelectWidget.html "OO.ui.RadioSelectWidget")
A radio group: several radio items in one group, mutually exclusive with at most one selected.
## Basic usage
The selected value uses the `value` / `defaultValue` / `onChange` channels. Clearing the current selection never produces a new value — `onChange`'s argument is always the newly selected `value`.
```tsx preview
import { useState } from "react";
import { RadioSelect } from "ooui-react";
const options = [
{ value: "small", children: "Small" },
{ value: "medium", children: "Medium" },
{ value: "large", children: "Large" },
];
function App() {
const [size, setSize] = useState("medium");
return (
Current selection: {String(size)}
);
}
export default App;
```
## With FieldLayout
To give the whole group a field label, wrap it with [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md); the label is associated with the group container, and clicking it focuses the group.
```tsx preview
import { RadioSelect, FieldLayout } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Disabled
- An option's own `disabled` skips that item (not selectable).
- Component-level `disabled` disables every option in the group, and the group state wins. See [Selection and options · Disabled](https://ooui-react.pages.dev/en/guide/options.md#disabled).
```tsx preview
import { RadioSelect } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Keyboard and focus
- Focus rests on the **whole group** (individual items are not in the Tab order). ↑↓←→ move **cyclically between non-disabled items and select directly**; Enter re-affirms the current item.
- On first Tab focus to the group with nothing selected, the first non-disabled item is auto-selected (matching native radio-group behavior).
- `onChange`'s second argument is the native `change` event; changing selection via arrow keys does not fire a native `change` (matching the original), so the second argument may be absent.
## API
| Prop | Description | Type | Default |
| -------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------- |
| `options` | The option set; each item has `value` and `children` (the option text) | `RadioSelectOptionProps[]` | — |
| `value` | Current selected value (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial selected value for uncontrolled use | `string \| number` | — |
| `onChange` | Selected-value change callback (value-first, incl. native event) | `ChangeHandler` | — |
| `name` | Form field name, passed to each option's native `radio` (same name = mutually exclusive) | `string` | — |
| `disabled` | Whether disabled (also disables every option in the group) | `boolean` | `false` |
| `...rest` | Native `div` props (`className`, `id`, `aria-*`, etc.) passed straight to the group container | `HTMLAttributes` | — |
## See also
- A single radio: [RadioInput](https://ooui-react.pages.dev/en/components/radio-input/index.md) (this component is built from it item by item; use it alone for custom layout)
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
- The checkbox counterpart: [CheckboxMultiselect](https://ooui-react.pages.dev/en/components/checkbox-multiselect/index.md)
---
url: https://ooui-react.pages.dev/en/components/tag-multiselect/index.md
---
> 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.
# TagMultiselect
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/TagMultiselect) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.TagMultiselectWidget.html "OO.ui.TagMultiselectWidget")
A tag input: values are shown as removable chips. Free-entry form — for "type to filter + dropdown candidates" use [MenuTagMultiselect](https://ooui-react.pages.dev/en/components/menu-tag-multiselect/index.md).
## Basic usage
Type text in the input and press **Enter**
to commit a tag; press **Backspace**
(when the input is empty) at the end to remove the previous tag; **click a tag**
to move it back into the input for editing. A tag's close button **only removes the tag**
— it does not also trigger the select/edit flow (opening the menu in menu mode, or backfilling text in edit mode). In the original, clicks on the close button bubble into those flows; this is an intentional deviation.
Free entry requires `allowArbitrary`: matching the original, without it (and without `allowedValues`) any typed value lies outside the legal range, so Enter is rejected (no error — simply no tag). See the next section for the whitelist form.
```tsx preview
import { useState } from "react";
import { TagMultiselect } from "ooui-react";
function App() {
const [tags, setTags] = useState<(string | number)[]>(["React", "TypeScript"]);
return (
Current tags: {tags.join(", ") || "(none)"}
);
}
export default App;
```
## Keyboard editing
- **Enter** commits the typed text as a tag; **Backspace** (when the inline input is empty) removes the last tag and **backfills its text into the input** for editing — holding Ctrl / Cmd removes without backfilling.
- **← / →**: with the caret at either end of the input, navigation moves between tags; moving forward past the last tag hands focus back to the input (directions flip in RTL); **Escape** clears the uncommitted text in the input.
## Whitelist and limits
- `allowedValues` is the legal value range; tags outside it are invalid (unless `allowArbitrary`).
- `allowDuplicates` permits repeated values; `tagLimit` caps the tag count, disabling input once reached.
- `allowDisplayInvalidTags` keeps invalid values shown as tags (flagged invalid) rather than refusing to add them.
```tsx preview
import { useState } from "react";
import { TagMultiselect } from "ooui-react";
function App() {
const [tags, setTags] = useState<(string | number)[]>([]);
return (
);
}
export default App;
```
Typing something outside the range — say `purple` — and pressing Enter still creates a tag, flagged invalid (the `allowDisplayInvalidTags` effect; without it the value is refused outright); once `tagLimit` of 2 is reached the input is disabled. Note: the component's overall invalid flag (red border) is broader than `onInvalidTagsChange` — uncommitted text left in the input on blur also triggers it. If you see the red border without an `onInvalidTagsChange` callback, check for leftover text in the input first.
## Sanitizing external values: pickValidTags
The exported pure function `pickValidTags(values, { options?, allowedValues?, allowDuplicates? })`
returns the **valid subset**
of the given values — dropping values outside the legal range and duplicates (when `allowDuplicates`
is off), using the same rules as the component (pass the component's own props to get a set complementary to `onInvalidTagsChange`
). Use it to sanitize tags before backfilling external data into `value`
, or before submitting.
## Input position and reordering
`inputPosition`
is `inline`
(default, the input follows the last tag), `outline`
(the input is below the tag area) or `none`
(no input, display-only, paired with a controlled value). `allowReordering`
(on by default) enables drag-to-reorder; when off, new tags insert according to the order in `allowedValues`
. The original allows replacing the inner input via `config.input`
/ `inputWidget`
; here the input is built in and cannot be replaced.
```tsx preview
import { useState } from "react";
import { TagMultiselect } from "ooui-react";
function App() {
const [tags, setTags] = useState<(string | number)[]>(["ooui", "react"]);
return (
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| ------------------------------ | --------------------------------------------------------------------- | ----------------------------------------------- | ---------- |
| `value` | Tag value set (controlled; passing it enables controlled mode) | `(string \| number)[]` | — |
| `defaultValue` | Initial value for uncontrolled use | `(string \| number)[]` | — |
| `onChange` | Add/remove callback (value-first, returns the scalar array) | `ChangeHandler<(string \| number)[]>` | — |
| `allowedValues` | Legal-value whitelist (the constraint without a menu) | `(string \| number)[]` | — |
| `allowArbitrary` | Allow arbitrary values (skips the range test) | `boolean` | `false` |
| `allowDuplicates` | Allow repeated values | `boolean` | `false` |
| `allowReordering` | Allow drag reordering | `boolean` | `true` |
| `allowEditTags` | Allow clicking a tag to edit it in place | `boolean` | `true` |
| `allowDisplayInvalidTags` | Keep invalid values shown as tags (flagged invalid) | `boolean` | `false` |
| `tagLimit` | Maximum tag count | `number` | — |
| `inputPosition` | Input position | `'inline' \| 'outline' \| 'none'` | `'inline'` |
| `onInvalidTagsChange` | Invalid-tag set change callback (read-only, doesn't change the value) | `(invalidValues: (string \| number)[]) => void` | — |
| `placeholder` / `name` | Input placeholder / `name` | `string` | — |
| `icon` / `indicator` / `flags` | Icon / indicator / flags | — | — |
| `disabled` | Whether disabled | `boolean` | `false` |
| `...rest` | Native `div` props passed straight to the root | `HTMLAttributes` | — |
:::note Differences from OOUI
The original allows `tag.data`
to be any object; this library always keeps a tag's identity as the scalar `value`
, so the controlled array is serializable and diffable. To carry an object with a tag, use the `data`
field on [MenuTagMultiselect](https://ooui-react.pages.dev/en/components/menu-tag-multiselect/index.md)
options and look it up by `value`
in your own `options`
.
:::
## See also
- Tag input with a candidate menu: [MenuTagMultiselect](https://ooui-react.pages.dev/en/components/menu-tag-multiselect/index.md)
- The `label` / `labelText` / `data` option convention: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md#options-of-tag-inputs)
---
url: https://ooui-react.pages.dev/en/components/menu-tag-multiselect/index.md
---
> 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.
# MenuTagMultiselect
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/MenuTagMultiselect) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.MenuTagMultiselectWidget.html "OO.ui.MenuTagMultiselectWidget")
A tag input with a candidate menu. Props match [TagMultiselect](https://ooui-react.pages.dev/en/components/tag-multiselect/index.md), plus the required `options`.
## Basic usage
Typing filters the candidates, ↑↓ moves the highlight and Enter chooses the highlighted item (with no highlight, the typed text is committed); without `allowArbitrary`, typed text outside the candidate value range is rejected (no error — simply no tag). A candidate's own `disabled` marks it as disabled — it cannot be added as a tag. See the next section for free entry.
```tsx preview
import { useState } from "react";
import { MenuTagMultiselect } from "ooui-react";
const options = [
{ value: "bold", label: "Bold", icon: "bold" },
{ value: "italic", label: "Italic", icon: "italic" },
{ value: "link", label: "Link", icon: "link" },
{ value: "image", label: "Image", icon: "image" },
];
function App() {
const [tags, setTags] = useState<(string | number)[]>(["bold"]);
return (
Selected: {tags.join(", ") || "(none)"}
);
}
export default App;
```
## Toggle semantics and tag editing
Menu candidates are **toggle-style**
: pressing Enter / clicking an already-chosen candidate again **removes**
that tag — the same candidate is both the add and the remove entry, and menu items with a corresponding tag show as selected (tags added by typing are marked selected as well).
Clicking a tag behaves differently depending on `allowArbitrary`: without it, clicking a tag locates it in the menu (opens the menu and highlights the matching candidate), and [TagMultiselect](https://ooui-react.pages.dev/en/components/tag-multiselect/index.md)'s `allowEditTags` backfill-to-edit doesn't take effect; with `allowArbitrary`, clicking a tag moves its text back into the input for editing.
:::note Differences from OOUI
With `inputPosition='none'`
(no input), clicking a tag also opens the menu and highlights the clicked candidate — the original has no path at all to open the menu in that form; keyboard navigation is not yet available once opened.
:::
## Value range and arbitrary values
Without `allowArbitrary`, the legal range for tags is the union of `allowedValues` and each option's `value` — usually only `options` is given, i.e. you can only add candidate values. With `allowArbitrary`, you can also type free text outside the candidates as a tag. `clearInputOnChoose` (on by default) controls whether the input's filter text is cleared after choosing a menu item.
```tsx preview
import { useState } from "react";
import { MenuTagMultiselect } from "ooui-react";
function App() {
const [tags, setTags] = useState<(string | number)[]>([]);
return (
);
}
export default App;
```
## Rich-content tags
An option's `label` accepts a `ReactNode`; when it is rich content (not a plain string), the plain text needed for prefix filtering and backfill must be provided explicitly via `labelText`, or that option won't participate in prefix filtering (see [Selection and options · Options of tag inputs](https://ooui-react.pages.dev/en/guide/options.md#options-of-tag-inputs)). Tags with `fixed: true` are fixed and can't be removed.
```tsx preview
import { useState } from "react";
import { MenuTagMultiselect } from "ooui-react";
function App() {
const [tags, setTags] = useState<(string | number)[]>(["template"]);
return (
Template (regular page)
>
),
labelText: "Template",
fixed: true,
},
{
value: "redirect",
label: (
<>
Redirect (points to another page)
>
),
labelText: "Redirect",
},
]}
/>
);
}
export default App;
```
The "Template" tag comes from the `fixed: true` candidate and shows the fixed marker (no close button); typing "re" filters to the "Redirect" candidate via `labelText` and backfills the plain text.
## API
On top of all [TagMultiselect](https://ooui-react.pages.dev/en/components/tag-multiselect/index.md) props:
| Prop | Description | Type | Default |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- |
| `options` | Candidate menu option set (**required**); fields in [Selection and options](https://ooui-react.pages.dev/en/guide/options.md#options-of-tag-inputs) | `MenuTagMultiselectOptionProps[]` | — |
| `clearInputOnChoose` | Whether to clear the filter text after choosing a menu item | `boolean` | `true` |
## See also
- Free entry without a candidate menu: [TagMultiselect](https://ooui-react.pages.dev/en/components/tag-multiselect/index.md)
- Option data and the keyboard focus model: [Selection and options](https://ooui-react.pages.dev/en/guide/options.md)
---
url: https://ooui-react.pages.dev/en/components/button-input/index.md
---
> 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.
# ButtonInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/ButtonInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.ButtonInputWidget.html "OO.ui.ButtonInputWidget")
A form button: renders a real `` / ` ` element whose `type` / `name` / `value` / `formNoValidate` take part in native form submission. Use [Button](https://ooui-react.pages.dev/en/components/button/index.md) when you have no submission needs.
## Basic usage
Drop it into a [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md) to trigger native submission; a `type="submit"` button carries its own `name` / `value` — whichever is clicked is the one submitted.
```tsx preview
import { useState } from "react";
import { ButtonInput, FormLayout, HiddenInputWidget } from "ooui-react";
function App() {
const [submitted, setSubmitted] = useState("");
return (
{
event.preventDefault();
const pairs: string[] = [];
new FormData(event.currentTarget).forEach((value, key) => {
pairs.push(`${key}=${value}`);
});
setSubmitted(pairs.join(" & "));
}}
>
Save
Save draft (skip validation)
{submitted && Submitted: {submitted}
}
);
}
export default App;
```
## The input form
`useInputTag` renders an ` ` instead of a ``: the label text doubles as the submitted value; icons, indicators and the `value` prop are not supported (the label must be plain text).
```tsx preview
import { ButtonInput } from "ooui-react";
function App() {
return (
Save
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| --------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ---------- |
| `children` | Button text | `ReactNode` | — |
| `type` | Native `type` attribute | `'button' \| 'submit' \| 'reset'` | `'button'` |
| `name` / `value` | Submitted field name and value (ignored with `useInputTag`; the ` `'s value is the label) | `string` | — |
| `formNoValidate` | Skip form validation on submit | `boolean` | `false` |
| `useInputTag` | Render as ` ` instead of `` | `boolean` | `false` |
| `flags` | Extra flags (color and form) | `ButtonFlag \| ButtonFlag[]` | `[]` |
| `framed` | Whether framed | `boolean` | `true` |
| `active` | Whether in the active state | `boolean` | `false` |
| `icon` / `indicator` | Icon / indicator (unsupported with `useInputTag`) | `string` / `'clear' \| 'up' \| 'down' \| 'required'` | — |
| `invisibleLabel` | Hide the label visually but keep it as the accessible name | `boolean` | `false` |
| `onClick` | Click handler (Enter/Space on the keyboard fire it too) | `(ev) => void` | — |
| `onMouseDown` / `onMouseUp` | Mouse-down / -up handlers; down enters and up resets the pressed state (not entered while disabled) | `(ev) => void` | — |
| `onKeyDown` / `onKeyUp` | Key-down / -up handlers; Enter / Space enter and reset the pressed state, other keys only forward the event | `(ev) => void` | — |
| `accessKey` | Access key (goes on the real `button` / `input`) | `string` | — |
| `disabled` | Whether disabled | `boolean` | `false` |
| `...rest` | Native `span` props passed straight to the root | `HTMLAttributes` | — |
The pressed-state class (`oo-ui-buttonElement-pressed`) shows while pressed via mouse or keyboard, matching the original.
## See also
- The button without submission semantics: [Button](https://ooui-react.pages.dev/en/components/button/index.md)
- The form container: [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md)
- Select fields that submit natively: [DropdownInput](https://ooui-react.pages.dev/en/components/dropdown-input/index.md)
---
url: https://ooui-react.pages.dev/en/components/dropdown-input/index.md
---
> 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.
# DropdownInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/DropdownInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.DropdownInputWidget.html "OO.ui.DropdownInputWidget")
A dropdown select field: a Dropdown handles display and interaction while an embedded hidden `` carries the form submission (`name` goes on it).
## Basic usage
The value is passed through the `value` / `defaultValue` / `onChange` props. A value not among the selectable options falls back to the first selectable one, so the component always holds a valid value.
```tsx preview
import { useState } from "react";
import { ButtonInput, DropdownInput, FormLayout } from "ooui-react";
function App() {
const [size, setSize] = useState("m");
const [submitted, setSubmitted] = useState("");
return (
{
event.preventDefault();
setSubmitted(new FormData(event.currentTarget).get("size")?.toString() ?? "");
}}
>
Submit
Current: {String(size)}; submitted: {submitted}
);
}
export default App;
```
When the passed value isn't among the options it falls back to the first selectable one — in the example below the initial `xl` is invalid, so the select shows "Small" and submits `s` as usual:
```tsx preview
import { useState } from "react";
import { ButtonInput, DropdownInput, FormLayout } from "ooui-react";
function App() {
const [submitted, setSubmitted] = useState("");
return (
{
event.preventDefault();
setSubmitted(new FormData(event.currentTarget).get("size")?.toString() ?? "");
}}
>
Submit
Submitted: {submitted || "(not submitted)"}
);
}
export default App;
```
## Grouped options
Options without `value` are group headings, mapped to `` elements in the hidden `` (`disabled` disables the whole group).
```tsx preview
import { DropdownInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
:::note
In the mobile form ([OOUIProvider](https://ooui-react.pages.dev/en/guide/configuration.md#mobile-mode)'s `isMobile`), the theme CSS hides the Dropdown and switches interaction to the native ``, matching the original's branch.
:::
## API
| Prop | Description | Type | Default |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------- |
| `options` | The option set: items with `value` are selectable (`children` is the display text, `disabled` disables), items without `value` are group headings | `DropdownInputOptionProps[]` | — |
| `value` | Current value (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial value for uncontrolled use | `string \| number` | — |
| `onChange` | Value change callback (the second argument is the native change event: carried on the mobile native `` path; choosing from the desktop menu has no native event and passes `undefined`) | `ChangeHandler` | — |
| `name` | Form field name (goes on the hidden ``) | `string` | — |
| `required` | Required (native `required`, carried by the hidden ``) | `boolean` | `false` |
| `disabled` | Whether disabled | `boolean` | `false` |
| `tabIndex` | Forwarded to the inner Dropdown's handle | `number` | — |
| `...rest` | Native `div` props passed straight to the root | `HTMLAttributes` | — |
## See also
- The interactive dropdown itself: [Dropdown](https://ooui-react.pages.dev/en/components/dropdown/index.md)
- The form container: [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md)
- The radio group field: [RadioSelectInput](https://ooui-react.pages.dev/en/components/radio-select-input/index.md)
---
url: https://ooui-react.pages.dev/en/components/checkbox-multiselect-input/index.md
---
> 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.
# CheckboxMultiselectInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/CheckboxMultiselectInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.CheckboxMultiselectInputWidget.html "OO.ui.CheckboxMultiselectInputWidget")
A checkbox group field: the form variant of [CheckboxMultiselect](https://ooui-react.pages.dev/en/components/checkbox-multiselect/index.md) — display and interaction unchanged, with each option's checkbox carrying `name` and its own `value`, so the checked ones submit natively as multiple same-name fields.
## Basic usage
The value channels match [CheckboxMultiselect](https://ooui-react.pages.dev/en/components/checkbox-multiselect/index.md): array-valued `value` / `defaultValue` / `onChange`.
```tsx preview
import { useState } from "react";
import { ButtonInput, CheckboxMultiselectInput, FormLayout } from "ooui-react";
const FRUITS = [
{ value: "apple", children: "Apple" },
{ value: "banana", children: "Banana" },
{ value: "cherry", children: "Cherry" },
];
function App() {
const [fruits, setFruits] = useState>(["apple"]);
const [submitted, setSubmitted] = useState("");
return (
{
event.preventDefault();
const values: string[] = [];
new FormData(event.currentTarget).forEach((value, key) => {
if (key === "fruits") {
values.push(String(value));
}
});
setSubmitted(values.join(", ") || "(none checked)");
}}
>
Submit
Submitted: {submitted}
);
}
export default App;
```
:::note Performance
The mapped option set is rebuilt only when the `options` reference changes: pass a stable reference (a module constant or `useMemo`) in hot paths instead of an inline array literal.
:::
## API
Inherits all props of [CheckboxMultiselect](https://ooui-react.pages.dev/en/components/checkbox-multiselect/index.md) (`options` / `value` / `defaultValue` / `onChange` / `name` / `disabled`, etc.); no new fields.
:::note
The options' checkbox `value` is taken over by this component (written with the option's own `value` for submission), so a value passed via `options[].checkboxProps.value` is overridden.
:::
## See also
- The interactive checkbox group (Shift+click range selection, etc.): [CheckboxMultiselect](https://ooui-react.pages.dev/en/components/checkbox-multiselect/index.md)
- The radio group field: [RadioSelectInput](https://ooui-react.pages.dev/en/components/radio-select-input/index.md)
- The form container: [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md)
---
url: https://ooui-react.pages.dev/en/components/radio-select-input/index.md
---
> 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.
# RadioSelectInput
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/RadioSelectInput) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.RadioSelectInputWidget.html "OO.ui.RadioSelectInputWidget")
A radio group field: a [RadioSelect](https://ooui-react.pages.dev/en/components/radio-select/index.md) handles display and keyboard interaction while an embedded hidden ` ` carries the form submission.
## Basic usage
Compared with [RadioSelect](https://ooui-react.pages.dev/en/components/radio-select/index.md): `name` is not written on each `radio` (exclusivity is guaranteed by the controlled state), and the component **always has a selection** — a value not among the selectable options falls back to the first selectable one, matching the "always one selected" semantics of HTML radio forms.
```tsx preview
import { useState } from "react";
import { ButtonInput, FormLayout, RadioSelectInput } from "ooui-react";
function App() {
const [notify, setNotify] = useState("email");
const [submitted, setSubmitted] = useState("");
return (
{
event.preventDefault();
setSubmitted(new FormData(event.currentTarget).get("notify")?.toString() ?? "");
}}
>
Submit
Current: {String(notify)}; submitted: {submitted}
);
}
export default App;
```
Invalid-value fallback: in the example below the initial `fax` is not among the options, so it falls back to the first selectable one ("Email") and submits `email` as usual:
```tsx preview
import { useState } from "react";
import { ButtonInput, FormLayout, RadioSelectInput } from "ooui-react";
function App() {
const [submitted, setSubmitted] = useState("");
return (
{
event.preventDefault();
setSubmitted(new FormData(event.currentTarget).get("notify")?.toString() ?? "");
}}
>
Submit
Submitted: {submitted || "(not submitted)"}
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | ------- |
| `options` | The option set; each item has `value` and `children` (the option text), `disabled` supported | `RadioSelectInputOptionProps[]` | — |
| `value` | Current value (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial value for uncontrolled use | `string \| number` | — |
| `onChange` | Value change callback (the second argument is the radio's native change event, carried on click reselection; keyboard reselection and the focus auto-select update silently, matching the original, with no event) | `ChangeHandler` | — |
| `name` | Form field name (written to the hidden `input` only) | `string` | — |
| `disabled` | Whether disabled (the hidden `input` is disabled too, dropping out of submission) | `boolean` | `false` |
| `tabIndex` | Forwarded to the inner RadioSelect's root | `number` | — |
| `...rest` | Native `div` props passed straight to the root | `HTMLAttributes` | — |
The original forwards only `data`
and `label`
when building options, silently dropping `options[].disabled`
; this implementation honors `disabled`
as declared, matching [CheckboxMultiselectInput](https://ooui-react.pages.dev/en/components/checkbox-multiselect-input/index.md)
and [DropdownInput](https://ooui-react.pages.dev/en/components/dropdown-input/index.md)
.
## See also
- The interactive radio group: [RadioSelect](https://ooui-react.pages.dev/en/components/radio-select/index.md)
- The dropdown field form: [DropdownInput](https://ooui-react.pages.dev/en/components/dropdown-input/index.md)
- The checkbox group field: [CheckboxMultiselectInput](https://ooui-react.pages.dev/en/components/checkbox-multiselect-input/index.md)
---
url: https://ooui-react.pages.dev/en/components/hidden-input-widget/index.md
---
> 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.
# HiddenInputWidget
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/widgets/HiddenInputWidget) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.HiddenInputWidget.html "OO.ui.HiddenInputWidget")
A hidden input: never shown, only carries a fixed value with the `
);
}
export default App;
```
:::note
The `expanded` (default) form fills the parent with absolute positioning — the parent must position and size itself; `expanded={false}` is the static form that shrinks to content (the demos above use it). Menu width and placement details come from the theme CSS. For finished components with full menu interaction, see [BookletLayout](https://ooui-react.pages.dev/en/components/booklet-layout/index.md) and [IndexLayout](https://ooui-react.pages.dev/en/components/index-layout/index.md).
:::
## API
Inherits all [Layout](https://ooui-react.pages.dev/en/components/layout/index.md) props (including the `hidden` three states), plus:
| Prop | Description | Type | Default |
| -------------- | --------------------------------------------------------------------- | ------------------------------------------ | ---------- |
| `menu` | The menu area content | `ReactNode` | — |
| `menuPosition` | Menu position (invalid values fall back to `before`) | `'before' \| 'after' \| 'top' \| 'bottom'` | `'before'` |
| `showMenu` | Whether to show the menu (the subtree is not rendered when collapsed) | `boolean` | `true` |
| `expanded` | Whether to fill the parent (`false` is the static form) | `boolean` | `true` |
## See also
- Finished components built from it: [BookletLayout](https://ooui-react.pages.dev/en/components/booklet-layout/index.md), [IndexLayout](https://ooui-react.pages.dev/en/components/index-layout/index.md)
- The `hidden` three-state semantics: [Layout](https://ooui-react.pages.dev/en/components/layout/index.md)
---
url: https://ooui-react.pages.dev/en/components/stack-layout/index.md
---
> 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.
# StackLayout
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/layouts/StackLayout) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.StackLayout.html "OO.ui.StackLayout")
A stack layout: renders a set of [pages](https://ooui-react.pages.dev/en/components/page-layout/index.md) from `options`, showing only the active page at a time; with `continuous`, all pages are visible and the container scrolls. Also the inner page stack of [BookletLayout](https://ooui-react.pages.dev/en/components/booklet-layout/index.md).
## Basic usage
The active page is controlled by `value` / `defaultValue` / `onChange`; an option is a [PageLayout](https://ooui-react.pages.dev/en/components/page-layout/index.md) prop set plus `value` (which doubles as the match key and the list key).
```tsx preview
import { useState } from "react";
import { Button, StackLayout } from "ooui-react";
const options = [
{ value: "a", label: "First page", padded: true, children: "First page content" },
{ value: "b", label: "Second page", padded: true, children: "Second page content" },
];
function App() {
const [page, setPage] = useState("a");
return (
setPage("a")}>First page
setPage("b")}>Second page
);
}
export default App;
```
:::note Form requirement
In the `expanded` form (on by default) the layout fills its parent via absolute positioning, so the parent must be positioned and sized (the demo wrapper above exists for exactly that); the whole-container scrolling of `continuous` depends on the same requirement. The `expanded={false}` static form works only for [MenuLayout](https://ooui-react.pages.dev/en/components/menu-layout/index.md)'s own flow content — this component's pages are absolutely positioned panels with no content height in static mode, so don't use it here.
:::
When the active value is missing or stale (e.g. its page was removed), a page is auto-selected: the next surviving page, or the new last page when the last one was removed; when no value was ever given or the value is not among the options, the first usable page is taken.
## Continuous mode
`continuous` makes all pages visible with the container scrolling as one; `onPageFocus` fires when focus enters a page (without changing the active value), which you can use for scroll-linked page selection.
```tsx preview
import { useState } from "react";
import { StackLayout } from "ooui-react";
function App() {
const [focused, setFocused] = useState("");
return (
setFocused(String(value))}
options={[
{ value: "a", label: "First page", padded: true, children: "First page content" },
{ value: "b", label: "Second page", padded: true, children: "Second page content" },
{ value: "c", label: "Third page", padded: true, children: "Third page content" },
]}
/>
Last focused page: {focused || "(click a page to try)"}
);
}
export default App;
```
## API
Inherits all [PanelLayout](https://ooui-react.pages.dev/en/components/panel-layout/index.md) props, plus:
| Prop | Description | Type | Default |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------ |
| `options` | The page set: [PageLayout](https://ooui-react.pages.dev/en/components/page-layout/index.md) props plus `value` | `(PageLayoutProps & { value: string \| number })[]` | — |
| `value` | Current active page (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial active page for uncontrolled use | `string \| number` | — |
| `onChange` | Active-page change callback | `ChangeHandler` | — |
| `onPageFocus` | Fires when focus enters a page (with the page value; for focus linkage, does not change the active value) | `(value: string \| number, event: FocusEvent) => void` | — |
| `continuous` | Whether all pages show (`true` ignores `value`) | `boolean` | `false` |
| `scrollable` | Whether scrollable (defaults follow `continuous`: the container scrolls in continuous mode, pages scroll individually otherwise) | `boolean` | `continuous` |
## See also
- The fields of a single page: [PageLayout](https://ooui-react.pages.dev/en/components/page-layout/index.md)
- The wrapper with an outline menu: [BookletLayout](https://ooui-react.pages.dev/en/components/booklet-layout/index.md)
- The controlled-value convention: [Controlled and uncontrolled](https://ooui-react.pages.dev/en/guide/controlled.md)
---
url: https://ooui-react.pages.dev/en/components/booklet-layout/index.md
---
> 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.
# BookletLayout
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/layouts/BookletLayout) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.BookletLayout.html "OO.ui.BookletLayout")
A booklet layout: with `outlined`, an [outline select](https://ooui-react.pages.dev/en/components/outline-select/index.md) on the left and a [page stack](https://ooui-react.pages.dev/en/components/stack-layout/index.md) on the right; without it, a plain stacked panel. Suits settings pages and multi-section content.
## Basic usage
An `options` item is `{ value, label, children }` plus the [PageLayout](https://ooui-react.pages.dev/en/components/page-layout/index.md) panel fields; the active page is controlled through `value` / `defaultValue` / `onChange`.
```tsx preview
import { useState } from "react";
import { BookletLayout } from "ooui-react";
function App() {
const [page, setPage] = useState("intro");
return (
);
}
export default App;
```
:::note Form premise
In the `expanded` form (on by default) the layout fills its parent via absolute positioning, so the parent must be positioned and sized (the demo wrapper above exists for exactly that). The `expanded={false}` static form works only for [MenuLayout](https://ooui-react.pages.dev/en/components/menu-layout/index.md)'s own flow content — the panels in the page stack are absolutely positioned panels with no content height in static mode, so don't use it here. The `editable` outline's select widget and controls are absolutely positioned in the theme CSS, likewise usable only in the expanded form.
:::
## Editable outline
`editable`
shows move-up/move-down/remove controls at the bottom of the outline: each option declares its own `movable`
/ `removable`
ability, the operations are handed back via `onMoveOption`
/ `onRemoveOption`
for the caller to update `options`
, and `outlineControlsExtra`
adds extra buttons beside them (such as "Add").
```tsx preview
import { useState } from "react";
import { BookletLayout, Button } from "ooui-react";
function App() {
const [pages, setPages] = useState([
{ value: "a", label: "Section 1", movable: true, removable: true, padded: true, children: "Section 1 content" },
{ value: "b", label: "Section 2", movable: true, removable: true, padded: true, children: "Section 2 content" },
{ value: "c", label: "Section 3", movable: true, removable: true, padded: true, children: "Section 3 content" },
]);
const move = (value: string | number, direction: -1 | 1) => {
setPages((prev) => {
const index = prev.findIndex((item) => item.value === value);
const target = index + direction;
if (target < 0 || target >= prev.length) {
return prev;
}
const next = [...prev];
[next[index], next[target]] = [next[target], next[index]];
return next;
});
};
return (
setPages((prev) => prev.filter((item) => item.value !== value))
}
outlineControlsExtra={
setPages((prev) => [
...prev,
{
value: `page-${prev.length + 1}`,
label: `New section ${prev.length + 1}`,
movable: true,
removable: true,
padded: true,
children: "New section content",
},
])
}
/>
}
/>
);
}
export default App;
```
## Continuous mode and auto-focus
`continuous` makes all pages visible and smooth-scrolls to the target page when switching; in continuous mode, focus entering a page (via Tab or clicking a focusable element inside it) conversely selects that page as the active one. `autoFocus` (default `true`) focuses the first focusable element inside the page after every switch (the first frame does not grab focus; skipped when focus is already inside the page, and suppressed in the mobile form). Both can be turned off.
When the active page is removed, a page is auto-selected: the next surviving page, or the new last page when the last one was removed — the outline and the page stack stay in sync.
## API
Inherits all [MenuLayout](https://ooui-react.pages.dev/en/components/menu-layout/index.md) props (except `menu` / `children`), plus:
| Prop | Description | Type | Default |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | ------- |
| `options` | The page set: `value`, `label`, `movable`, `removable` plus [PageLayout](https://ooui-react.pages.dev/en/components/page-layout/index.md) fields | array | — |
| `value` | Current active page (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial active page for uncontrolled use | `string \| number` | — |
| `onChange` | Active-page change callback | `ChangeHandler` | — |
| `outlined` | Whether to show the outline (`false` is the plain stacked mode) | `boolean` | `false` |
| `continuous` | Whether all pages show continuously, scrolling to the target on switch | `boolean` | `false` |
| `autoFocus` | Focus the page's first focusable element after switching (not on the first frame) | `boolean` | `true` |
| `editable` | Whether to show the outline controls (requires `outlined`) | `boolean` | `false` |
| `onMoveOption` | Move-up/down click handler in editable mode; `direction` is `-1` (up) or `1` (down) | `(value: string \| number, direction: -1 \| 1) => void` | — |
| `onRemoveOption` | Remove click handler in editable mode | `(value: string \| number) => void` | — |
| `outlineControlsExtra` | Extra button area left of the outline controls in editable mode | `ReactNode` | — |
## See also
- The page stack on the right: [StackLayout](https://ooui-react.pages.dev/en/components/stack-layout/index.md)
- The outline select on the left: [OutlineSelect](https://ooui-react.pages.dev/en/components/outline-select/index.md)
- The top-tabs variant: [IndexLayout](https://ooui-react.pages.dev/en/components/index-layout/index.md)
---
url: https://ooui-react.pages.dev/en/components/index-layout/index.md
---
> 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.
# IndexLayout
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/layouts/IndexLayout) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.IndexLayout.html "OO.ui.IndexLayout")
An index layout: a [tab select](https://ooui-react.pages.dev/en/components/tab-select/index.md) on top and a stack of [tab panels](https://ooui-react.pages.dev/en/components/tab-panel-layout/index.md) below. Suits settings pages — "top tabs + chunked content"; for the side-outline variant see [BookletLayout](https://ooui-react.pages.dev/en/components/booklet-layout/index.md).
## Basic usage
An `options` item is `{ value, label, children }` plus the [TabPanelLayout](https://ooui-react.pages.dev/en/components/tab-panel-layout/index.md) panel fields (with `disabled` to disable a tab); the active tab is controlled through `value` / `defaultValue` / `onChange`. Tabs and panels are wired with `aria-controls` / `aria-labelledby` automatically.
```tsx preview
import { useState } from "react";
import { IndexLayout } from "ooui-react";
function App() {
const [tab, setTab] = useState("general");
return (
);
}
export default App;
```
:::note Form premise
In the `expanded` form (on by default) the layout fills its parent via absolute positioning, so the parent must be positioned and sized (the demo wrapper above exists for exactly that). The `expanded={false}` static form works only for [MenuLayout](https://ooui-react.pages.dev/en/components/menu-layout/index.md)'s own flow content — the panel stacks of this component and BookletLayout are absolutely positioned panels with no content height in static mode, so don't use it here.
:::
## Browser-search linkage
`openMatchedPanels` (default `true`) hides inactive panels with `hidden="until-found"`: when the browser's in-page search (Ctrl+F) matches content inside a hidden panel, the corresponding tab is activated automatically. **Under `continuous` all panels show at once and `openMatchedPanels` is disabled** — panels are no longer hidden with until-found and the browser-search linkage stops. `autoFocus` (default `true`) focuses the panel's first focusable element after a switch (suppressed in the mobile form).
When the active tab is removed, the first selectable (non-disabled) tab is auto-selected.
Under `continuous`, all panels are visible at once, stacked vertically (useful for printing and full-text reading):
```tsx preview
import { useState } from "react";
import { IndexLayout } from "ooui-react";
function App() {
const [tab, setTab] = useState("intro");
return (
);
}
export default App;
```
## API
Inherits all [MenuLayout](https://ooui-react.pages.dev/en/components/menu-layout/index.md) props (except `menu` / `menuPosition` / `children`), plus:
| Prop | Description | Type | Default |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- |
| `options` | The tab set: `value`, `label`, `disabled` plus [TabPanelLayout](https://ooui-react.pages.dev/en/components/tab-panel-layout/index.md) fields (a disabled tab's panel stays fully `hidden` regardless of `openMatchedPanels`) | `IndexLayoutTabProps[]` | — |
| `value` | Current active tab (controlled; passing it enables controlled mode) | `string \| number` | — |
| `defaultValue` | Initial active tab for uncontrolled use | `string \| number` | — |
| `onChange` | Active-tab change callback | `ChangeHandler` | — |
| `framed` | Whether the tabs are framed | `boolean` | `true` |
| `continuous` | Whether all panels show | `boolean` | `false` |
| `autoFocus` | Focus the panel's first focusable element after switching | `boolean` | `true` |
| `openMatchedPanels` | Hide inactive panels with `hidden="until-found"` and switch tabs when the browser search matches (disabled under `continuous`) | `boolean` | `true` |
## See also
- The fields of a single tab panel: [TabPanelLayout](https://ooui-react.pages.dev/en/components/tab-panel-layout/index.md)
- The side-outline variant: [BookletLayout](https://ooui-react.pages.dev/en/components/booklet-layout/index.md)
- The `hidden="until-found"` three states: [Layout](https://ooui-react.pages.dev/en/components/layout/index.md)
---
url: https://ooui-react.pages.dev/en/components/tab-panel-layout/index.md
---
> 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.
# TabPanelLayout
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/layouts/TabPanelLayout) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.TabPanelLayout.html "OO.ui.TabPanelLayout")
A tab panel: an item of [IndexLayout](https://ooui-react.pages.dev/en/components/index-layout/index.md)'s `options` array, with `role="tabpanel"` on the root. You rarely render it directly.
## Standalone use
`active` declares the active state (outputs the `oo-ui-tabPanelLayout-active` class). `label` / `value` / `disabled` are fields carried along with the tab-set object — the component consumes them, so they are never written as DOM attributes. The panel defaults to `expanded` (fills its parent via absolute positioning), so when placed on its own it needs a positioned, sized parent.
```tsx preview
import { IndexLayout } from "ooui-react";
function App() {
return (
);
}
export default App;
```
Rendered without an IndexLayout, the active state is declared via `active` (the root emits the `oo-ui-tabPanelLayout-active` class; panels default to `expanded`, filling a positioned parent — pass `expanded={false}` to place them statically):
```tsx preview
import { TabPanelLayout } from "ooui-react";
function App() {
return (
General panel: declared with active, the root emits the oo-ui-tabPanelLayout-active class
Advanced panel: fully hidden while hidden
);
}
export default App;
```
## API
Inherits all [PanelLayout](https://ooui-react.pages.dev/en/components/panel-layout/index.md) props, plus:
| Prop | Description | Type | Default |
| ------------------------------ | ---------------------------------------------------------- | -------------------------------------------- | ------- |
| `active` | Whether this is the active panel | `boolean` | `false` |
| `scrollable` | Whether scrollable (panels scroll by default) | `boolean` | `true` |
| `label` / `value` / `disabled` | Carried with the tab-set object, consumed by the component | `ReactNode` / `string \| number` / `boolean` | — |
## See also
- The tab container (with the tabpanel association): [IndexLayout](https://ooui-react.pages.dev/en/components/index-layout/index.md)
---
url: https://ooui-react.pages.dev/en/components/field-layout/index.md
---
> 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.
# FieldLayout
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/layouts/FieldLayout) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.FieldLayout.html "OO.ui.FieldLayout")
Field layout: a container that places a field control together with its label — the basic unit inside a [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md).
## Basic usage
The label/control association is done automatically: an input control placed inside `FieldLayout` claims an `id`, and the label's `htmlFor` points at it, so clicking the label focuses the control and screen readers read the label. The field control itself does **not** need to set `label`.
```tsx preview
import { TextInput, FieldLayout } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Alignment
`align`
decides how the label and control are laid out: `left`
(default, label on the left), `right`
(label on the right), `top`
(label above), and `inline`
(label and control on the same line, commonly for checkboxes and radios). Passing `inline`
is not auto-downgraded (the original falls back to `top`
when the field root element isn't a `span`
); make sure the field root element is inline before passing it.
```tsx preview
import { CheckboxInput, FieldLayout, TextInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Label and tooltip
- `label` is the label content (`ReactNode`).
- `invisibleLabel` hides the label visually but keeps the accessible name; then `title` falls back to the label text when not given explicitly.
- `title` goes on the **label element** (matching the original `$titled = $label`), not on the layout root.
- When the field control registers its own `accessKey`, the label tooltip gets the key appended (the chord text can be localized via [OOUIProvider](https://ooui-react.pages.dev/en/guide/configuration.md)).
```tsx preview
import { FieldLayout, TextInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Disabled
`FieldLayout`'s `disabled` only adds the `oo-ui-fieldLayout-disabled` style class to the layout root, giving the field area a disabled look — it **does not disable the control in `children` for you**. To actually disable it, set `disabled` on the control itself; to disable a whole group, use `FieldsetLayout`, which relies on a native ``.
```tsx preview
import { FieldLayout, TextInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| ---------------- | ---------------------------------------------------------------------------------- | ---------------------------------------- | -------- |
| `children` | The field control | `ReactNode` | — |
| `label` | Label content | `ReactNode` | — |
| `invisibleLabel` | Label visually hidden (kept as accessible name) | `boolean` | `false` |
| `align` | Label alignment | `'left' \| 'right' \| 'top' \| 'inline'` | `'left'` |
| `title` | Tooltip text for the label (goes on the label element) | `string` | — |
| `disabled` | Adds a disabled style class to the layout (not passed to the control) | `boolean` | `false` |
| `...rest` | Native `div` props (`className`, `id`, `data-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
## See also
- The form container that wraps several `FieldLayout`s: [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md)
- Props and placement of the controls: [TextInput](https://ooui-react.pages.dev/en/components/text-input/index.md), [CheckboxInput](https://ooui-react.pages.dev/en/components/checkbox-input/index.md)
- The `disabled`, `title` and `accessKey` semantics shared by all components: [Common props](https://ooui-react.pages.dev/en/guide/basics.md)
---
url: https://ooui-react.pages.dev/en/components/fieldset-layout/index.md
---
> 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.
# FieldsetLayout
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/layouts/FieldsetLayout) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.FieldsetLayout.html "OO.ui.FieldsetLayout")
A field set: a native `` with a `` header (title, icon and help entry), with the field items inside the group container. The root is a native `fieldset`, so its `disabled` natively disables every input control inside the group.
## Basic usage
```tsx preview
import { FieldLayout, FieldsetLayout, TextInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Help text
How `help` is presented depends on `helpInline`: `helpInline={false}` (default) renders an ⓘ icon in the header that opens a popup on click; `helpInline={true}` shows the help as inline text after the header.
```tsx preview
import { CheckboxInput, FieldLayout, FieldsetLayout } from "ooui-react";
function App() {
return (
);
}
export default App;
```
Here the default ⓘ popup and the inline text with `helpInline={true}` appear side by side:
```tsx preview
import { FieldLayout, FieldsetLayout, TextInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## Group-level disable
`disabled` goes on the native ` `, so every input control inside the group is disabled by the browser's own mechanism — no per-item passing needed.
```tsx preview
import { CheckboxInput, FieldLayout, FieldsetLayout, TextInput } from "ooui-react";
function App() {
return (
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| ---------------- | --------------------------------------------------------------- | ------------------------------------- | ------- |
| `label` | The group title (legend header) | `ReactNode` | — |
| `icon` | Header icon | `string` | — |
| `help` | Help text | `ReactNode` | — |
| `helpInline` | Whether help is inline (`false` renders the ⓘ popup) | `boolean` | `false` |
| `invisibleLabel` | Hide the title visually but keep it as the accessible name | `boolean` | `false` |
| `disabled` | Group-level disable (propagated natively to every input inside) | `boolean` | `false` |
| `...rest` | Native `fieldset` props passed straight to the root | `HTMLAttributes` | — |
## See also
- Label and help for a single field: [FieldLayout](https://ooui-react.pages.dev/en/components/field-layout/index.md)
- The form container: [FormLayout](https://ooui-react.pages.dev/en/components/form-layout/index.md)
---
url: https://ooui-react.pages.dev/en/components/form-layout/index.md
---
> 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.
# FormLayout
> [Source](https://github.com/BearBin1215/ooui-react/tree/main/src/layouts/FormLayout) | [Original component](https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.FormLayout.html "OO.ui.FormLayout")
Form container: it renders a `
),
},
},
]}
/>
);
}
export default App;
```
## List and Menu tool groups
Both consist of a handle plus a dropdown panel (the handle's icon/label/indicator are configurable; the indicator defaults to flipping with `position`: pointing up on a bottom toolbar), and the panel can show a `header` at the top.
**ListToolGroup** holds tools as a vertical text list with a collapse system: tools named in `allowCollapse` start collapsed (visible only once the panel is expanded), and a More / Fewer toggle appears at the bottom of the panel automatically (choosing it keeps the panel open; its text and icon switch with the state); `forceExpand` declares it the other way — the listed tools are always visible and everything else is collapsible. The name list is intersected with the actual `tools`; invalid names produce no toggle. The expanded state is managed via `expanded` (controlled) / `defaultExpanded` (initial value for uncontrolled use, collapsed by default).
**MenuToolGroup**
holds tools as a horizontal text list, and the handle label is composed from the **active tool**
's (`active: true`
) visible text joined with ", ", falling back to `label`
when none is active — a fit for exclusive menus that display the current choice, such as paragraph format or alignment.
Shared convention of the two groups: the panel opens and closes through the common `open`
/ `defaultOpen`
/ `onOpenChange`
controlled props (handle toggle, choosing a tool, outside click and Escape all fire it); `onToolSelect`
receives the tool definition whenever any tool is chosen, for unified dispatch; the panel closes automatically after a tool is chosen. A group-level `disabled`
disables every tool; **when all tools are disabled the group disables itself**
, and a group with empty `tools`
hides entirely — permission-driven scenarios that clear `tools`
dynamically need no extra handling. Escape closes the panel in the capture phase, so a toolbar nested in a Dialog won't close the dialog by mistake.
```tsx preview
import { useState } from "react";
import { ListToolGroup, Toolbar } from "ooui-react";
function App() {
const [active, setActive] = useState("p");
return (
setActive("p") },
{ name: "h", label: "Heading", active: active === "h", onSelect: () => setActive("h") },
{ name: "pre", label: "Preformatted", onSelect: () => {} },
{ name: "blockquote", label: "Block quote", onSelect: () => {} },
]}
/>
);
}
export default App;
```
The example below packs three pieces into one toolbar: a **Menu group** (its handle text is composed from the active tool — switch the alignment and the handle follows; it falls back to `label` when nothing is active), a **Label group** (a static text segment, usable as a visual divider) and a **nested tool group** (a Bar group's tool slot renders a whole List group via `group`, forming a "nested dropdown").
```tsx preview
import { useState } from "react";
import {
BarToolGroup,
LabelToolGroup,
ListToolGroup,
MenuToolGroup,
Toolbar,
} from "ooui-react";
function App() {
const [align, setAlign] = useState("left");
return (
setAlign("left"),
},
{
name: "center",
icon: "alignCenter",
label: "Center",
active: align === "center",
onSelect: () => setAlign("center"),
},
{
name: "right",
icon: "alignRight",
label: "Align right",
active: align === "right",
onSelect: () => setAlign("right"),
},
]}
/>
{} },
{ name: "image", icon: "image", label: "Image", onSelect: () => {} },
{
name: "gallery",
icon: "imageGallery",
label: "Gallery",
onSelect: () => {},
},
]}
/>
),
},
]}
/>
);
}
export default App;
```
## Narrow mode and position
When the toolbar isn't wide enough to tile all its content, it enters **narrow mode**: the theme CSS tightens the overall spacing; tools and handles can customize their narrow form via `narrowConfig` (hide labels, swap icons or text, restored on leaving narrow mode). `position` is `top` (default) or `bottom`, deciding whether popup panels open downward or upward.
The example below squeezes the toolbar into narrow mode with a fixed-width container: `bold` and the List handle each hide their text in narrow mode via `narrowConfig`; the toolbar also sits at the bottom (`position="bottom"`) — open the List panel to see it expand upward with the handle indicator pointing up.
```tsx preview
import { useState } from "react";
import { BarToolGroup, ListToolGroup, Toolbar } from "ooui-react";
function App() {
const [active, setActive] = useState("p");
return (
setActive("p"),
},
{
name: "h",
label: "Heading",
active: active === "h",
onSelect: () => setActive("h"),
},
]}
/>
);
}
export default App;
```
## API
| Prop | Description | Type | Default |
| ---------- | ---------------------------------------------------------------------------------- | -------------------------------- | ------- |
| `children` | Tool groups (`BarToolGroup`, `ListToolGroup`, `MenuToolGroup`, etc.) | `ReactNode` | — |
| `position` | Toolbar position; affects popup direction and class names | `'top' \| 'bottom'` | `'top'` |
| `actions` | Content of the separate actions area on the right | `ReactNode` | — |
| `...rest` | Native `div` props (`className`, `id`, `data-*`, etc.) passed straight to the root | `HTMLAttributes` | — |
Common fields of a tool (an element of the `tools` array):
- `name` (symbol name, required): contributes the `oo-ui-tool-name-{name}` class (a path form `'a/b/c'` takes its first two segments) — usable as a style or test hook.
- `label` (visible text) and `icon`: Bar groups show the icon only by default; the text shows via `displayBothIconAndLabel: true` (icon + text with an icon, a text-only button without one — without it an icon-less tool uses the original's raw icon-slot form); List / Menu groups always show the label. `title` is always a tooltip (Bar groups append `title` and `accelerator` to the link's title; List / Menu groups render only the accelerator slot and don't append it). {/* deviations: dev-tool-accelerator */}
- `active` (controlled active state), `onSelect` (fires on click or keyboard Enter / Space), `disabled`.
- `narrowConfig`: in narrow mode the **defined** fields replace the tool's icon / label form, restored on leaving narrow mode.
- `popup`: popup tool (see the previous section).
- `group`: a nested tool group — the tool slot renders a whole tool group instead of a tool link (for "nested dropdowns"); the handle and selection state are owned by the nested group, and `title` / `icon` / `active` / `onSelect` don't apply to this tool, only `disabled` carries over as the slot's disabled state; when both `popup` and `group` are given, `group` wins. {/* deviations: dev-toolgroup-tool-element */}
The full reference is in the tool-group components and the `ToolProps` type.
## Differences from OOUI
- **Tools are no longer registered through a `ToolFactory`**: tools are handed to the group as a declarative `tools` array, with the active state owned by the caller. {/* deviations: dev-tool-factory */}
- **The toolbar and tool groups have no event surface**: the equivalent of listening to `Toolbar.active` in the original is re-rendering the `tools` array with `active` declared; popup layers and panels likewise use controlled props and callbacks. {/* deviations: dev-toolbar-events */}
- **A popup tool's `onSelect` still fires**: in the original it is occupied by the popup toggle and the caller never hears about selections; here selection is independent of toggling, and visibility changes are reported via `popup.onOpenChange`. {/* deviations: dev-popup-tool-onselect */}
- **A popup tool's `autoFlip` defaults to off and can be enabled explicitly.** {/* deviations: dev-popup-tool-autoflip */}
- **Visible text and tooltip are split from one `title` key into `label` + `title`**: `label` is the visible text, `title` is always the tooltip; when migrating, pass the old `narrowConfig.title` value as `label`. {/* deviations: dev-tool-label-title */}
- **In narrow mode the handle's tooltip always falls back to the effective `label`.** {/* deviations: dev-popuptoolgroup-title */}
- **A panel that fills the container width is still height-clamped to the visible area with internal scrolling** (tools stay reachable). {/* deviations: dev-panel-maxheight */}
- **The narrow-mode measurement baseline is remeasured when tool groups are added or removed**, so dynamic tool sets keep correct narrow detection. {/* deviations: dev-narrow-threshold-reset */}
- **On a bottom toolbar (`position="bottom"`) the More / Fewer arrow directions are inverted compared to the original**; functionality is unaffected. {/* deviations: dev-listtoolgroup-icons */}
## See also
- The flat icon-button tool group: `BarToolGroup`
- Tool groups gathered into a dropdown: `ListToolGroup`, `MenuToolGroup`
- The static label tool group: `LabelToolGroup`
- Common pass-through and `ref` rules: [Common props](https://ooui-react.pages.dev/en/guide/basics.md)
---
url: https://ooui-react.pages.dev/en/index.md
---
> 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.
# ooui-react
> OOUI for React
[Get Started](/en/guide/usage) | [Components](/en/components/)
## Features
- /feature-icons/code-xml.svg **Structure parity**: Outputs the same DOM structure, class names and attributes as OOUI. Styles are loaded by the consumer from OOUI, making it a natural fit for MediaWiki sites.
- /feature-icons/keyboard.svg **Behavior parity**: Keyboard, focus, a11y and edge cases are checked one by one against OOUI, so interaction semantics match.
- /feature-icons/react.svg **Declarative API**: OOUI's imperative setters and events are turned into controlled props such as value and onChange, matching React conventions.
- /feature-icons/typescript.svg **Type support**: Written entirely in TypeScript, with complete type definitions.
- /feature-icons/preact.svg **Preact compatible**: Runs on the Preact compatibility layer to keep bundles small.
- /feature-icons/package.svg **Tree Shaking**: Import only what you use.
:::note About this translation
The English documentation is translated and proofread with AI assistance from the Chinese source, so some wording may lag the original or read imperfectly; please [report any inaccuracies on GitHub](https://github.com/BearBin1215/ooui-react/issues).
:::
## Try it live
```tsx file="/demos/en/advanced-search.tsx"
import { useState } from "react";
import { Button, Dropdown, TextInput } from "ooui-react";
import "./advanced-search.css";
const SEARCH_CODES = {
none: "None",
intitle: "Page title contains",
incategory: "Pages in these categories",
hastemplate: "Pages with these templates",
linksto: "Pages linking to",
insource: "Source text contains",
};
const OPTIONS = Object.entries(SEARCH_CODES).map(([value, label]) => ({
value,
children: label,
}));
function AdvancedSearchDemo() {
const [rows, setRows] = useState([{ code: "none", value: "" }]);
const addRow = () => setRows((prev) => [...prev, { code: "none", value: "" }]);
const setRow = (index: number, patch: { code?: string; value?: string }) =>
setRows((prev) => prev.map((row, i) => (i === index ? { ...row, ...patch } : row)));
return (
Advanced search
{rows.map(({ code, value }, index) => {
const pending = index === rows.length - 1;
return (
setRow(index, { code: String(next) })}
onFocus={pending ? addRow : undefined}
options={OPTIONS}
/>
setRow(index, { value: next })}
onFocus={pending ? addRow : undefined}
/>
setRows((prev) => prev.filter((_, i) => i !== index))}
/>
);
})}
);
}
export default AdvancedSearchDemo;
```
```css file="/demos/en/advanced-search.css"
.advsearch {
max-width: 50em;
box-sizing: border-box;
padding: 0.5em 0.75em;
border: 1px solid #c8ccd1;
background-color: #f8f9fa;
}
.advsearch-header {
font-size: 1.2em;
font-weight: 600;
}
.advsearch .advsearch-line {
display: flex;
gap: 8px;
margin-top: 0.4em;
}
.advsearch .advsearch-line-pending {
opacity: 0.4;
transition: opacity ease-out 0.3s;
}
.advsearch .advsearch-line-pending:hover {
opacity: 1;
}
.advsearch .advsearch-line-pending .advsearch-remove {
opacity: 0;
}
.advsearch .advsearch-code {
flex: 0 0 19em;
}
.advsearch .advsearch-value {
flex: 1 1 40em;
max-width: none;
}
```
Focus the trailing empty row to append another line, and the minus button at the end of a row removes it.
## Installation
```sh [npm]
npm install ooui-react
```
```sh [yarn]
yarn add ooui-react
```
```sh [pnpm]
pnpm add ooui-react
```
```sh [bun]
bun add ooui-react
```
```sh [deno]
deno add npm:ooui-react
```
React ≥ 18 is required; SSR is not supported yet. For loading styles, MediaWiki integration and Preact compatibility, see the [Usage guide](https://ooui-react.pages.dev/en/guide/usage.md).
## Why this library?
Back in 2023, while writing [my own Moegirlpedia gadget](https://github.com/BearBin1215/MoegirlPedia), I found that managing state with jQuery alone got painful in more complex scenarios, so I wanted to bring in a modern framework. At the time:
- MediaWiki was on 1.31, which shipped neither Vue nor Codex.
- Bundling Vue myself started at 60 KB, and writing a little more would push the bundle past what could be uploaded to the site without timing out.
- Most mature component libraries ship their own stylesheets, which also makes for large bundles.
- I lack the art skills to design the styles myself ~~(important!)~~.
So I went with [Preact](https://preactjs.com/) plus a self-built OOUI component library: a small framework, no stylesheet, rendering only DOM.
As AI-assisted programming took off, this project grew into a proper component library. Moegirlpedia has since upgraded MediaWiki and supports Vue + Codex, but I still think this library earns its place.