{
  "title": "Select",
  "description": "Searchable, async-capable, optionally virtualized select with custom item renderers, RTL, locale-aware sort, and OptGroup support.",
  "url": "/docs/components/select",
  "since": "0.2.0",
  "tags": [
    "input",
    "form"
  ],
  "platform": "both",
  "source": "---\ntitle: Select\ndescription: Searchable, async-capable, optionally virtualized select with custom item renderers, RTL, locale-aware sort, and OptGroup support.\nsince: 0.2.0\ntags: [input, form]\nplatform: both\ncategory: inputs\n---\n\nimport { BundleSize } from '@/components/bundle-size';\nimport { Preview } from '@/components/preview';\nimport { PropsTable } from '@/components/props-table';\n\n<BundleSize component=\"Select\" />\n\n## At a glance\n\nTwo operating modes:\n\n- **Static** — pass `options`. The picker filters in-memory by substring match on `label` (override via `filterOption`).\n- **Async** — pass `loadOptions(params)`. Called on search-input change (debounced ~150 ms) and when the list scrolls near the bottom for the next page. The component manages the loaded list and pagination cursor itself, so consumers don't have to.\n\nOther capabilities:\n\n- **Searchable** — auto-on for static lists ≥ 10 items and always on for async; override with `searchable`.\n- **Virtualized list** — auto-on for lists > 100 items; only the visible window of options is rendered.\n- **OptGroup support** — items with the same `group` field cluster under a group header.\n- **Locale-aware sort** — pass `locale=\"de\"` (or any BCP 47) and the picker re-sorts via `Intl.Collator`. Switch the locale and the list re-orders alphabetically for the new language.\n- **Custom item renderer** — pass `renderOption` for arbitrary content per row.\n- **RTL** — pass `dir=\"rtl\"`; popup alignment + text direction flip.\n- **Keyboard nav** — Arrow Down/Up move highlight, Enter selects, Escape closes, Tab closes.\n\n## Static — with groups\n\nItems with the same `group` field cluster under a header. Searchable auto-on once the list reaches 10 items.\n\n<Preview name=\"select-basic\" />\n\n## Multi-select\n\nPass `multiple` to switch the value / `onChange` types to arrays. The trigger renders a chip per selected option (collapsing to \"N selected\" past `maxChips`, default 3); the popup shows a checkbox-style indicator next to each option, stays open between picks, and exposes a \"Clear all\" affordance plus `aria-multiselectable=\"true\"`. Optional `maxSelected` caps the count — extra picks are silently ignored.\n\n<Preview name=\"select-multi\" />\n\n### Capped selection\n\nCap the number of selectable items with `maxSelected` — extra picks are ignored silently. Pair with a smaller `maxChips` so the trigger stays compact while the cap is approached. The example below caps at 3 and collapses to \"N selected\" past 2 chips.\n\n<Preview name=\"select-multi-capped\" />\n\n## Async + paginated\n\n`loadOptions` is called on every search change (debounced ~150ms) and on scroll near the bottom of the list. The component manages the loaded array and pagination cursor; consumers only return `{ items, total? }`.\n\n<Preview name=\"select-async\" />\n\n## Virtualized\n\nPass a thousand items in. Only the visible window is rendered (auto-on once the option count crosses 100).\n\n<Preview name=\"select-virtualized\" />\n\n## Custom item renderer\n\n`renderOption(option, { selected, active })` returns the JSX for each row. The picker still owns selection state, keyboard nav, and group headers — the custom renderer only owns the row's content.\n\n<Preview name=\"select-custom-renderer\" />\n\n## Locale-aware sort\n\n`locale=\"de\"` (or any BCP 47) re-sorts the list via `Intl.Collator`. Switch the locale and the order updates so the alphabet matches the language a reader is in. Pair with `sortByLocale={false}` to keep your preferred order while still benefiting from a locale-tagged accessibility tree.\n\n<Preview name=\"select-locale\" />\n\n## Use with Field\n\nFor labelled selects with description, error, and a11y wiring, wrap in `<Field>`:\n\n```tsx\nimport { Field, Select } from '@nori-ui/core';\n\nexport const Example = () => (\n    <Field label=\"Country\" description=\"Where we'll ship your order.\">\n        <Select placeholder=\"Pick a country\" options={[\n            { value: 'us', label: 'United States' },\n            { value: 'de', label: 'Germany' },\n            { value: 'jp', label: 'Japan' },\n        ]} />\n    </Field>\n);\n```\n\nWith validation error:\n\n```tsx\nimport { Field, Select } from '@nori-ui/core';\n\nexport const Example = () => (\n    <Field label=\"Country\" error=\"Please select a country.\">\n        <Select placeholder=\"Pick a country\" options={[\n            { value: 'us', label: 'United States' },\n            { value: 'de', label: 'Germany' },\n        ]} />\n    </Field>\n);\n```\n\nSee [Field](/docs/components/field) for the full API. For custom layouts (e.g., a control + button row), use the [compound API](/docs/components/field#custom-layout-compound-api).\n\n## `value` and `defaultValue`\n\nControlled via `value` (paired with `onChange`); uncontrolled via\n`defaultValue`. Single-mode types are `string`; multi-mode (`multiple`)\ntypes are `string[]`. Mixing the two prefers controlled.\n\n```tsx\n// Controlled single\n<Select value={planId} onChange={setPlanId} options={…} />\n\n// Uncontrolled multi\n<Select multiple defaultValue={['a', 'b']} options={…} />\n```\n\n## `placeholder`, `searchPlaceholder`, `noOptionsMessage`, `loadingMessage`\n\nEach of the user-visible labels is overridable. Use them to localize\nor to write copy that matches your product voice.\n\n| Prop                | Default       | Where it shows                                |\n|---------------------|---------------|-----------------------------------------------|\n| `placeholder`       | \"Select…\"     | Trigger when nothing is selected              |\n| `searchPlaceholder` | \"Search…\"     | Search input when `searchable` is on          |\n| `noOptionsMessage`  | \"No options\"  | Popup body when the filtered list is empty    |\n| `loadingMessage`    | \"Loading…\"    | Popup body while async results are pending    |\n\n```tsx\n<Select\n    placeholder=\"Pick a country\"\n    searchPlaceholder=\"Type to filter…\"\n    noOptionsMessage=\"No matches\"\n    options={…}\n/>\n```\n\n## `disabled`\n\nGreys the trigger, blocks pointer + keyboard interaction, and\nforwards `aria-disabled`. Individual options can also be disabled via\n`disabled: true` on the option.\n\n```tsx\n<Select disabled defaultValue=\"locked\" options={[{ value: 'locked', label: 'Locked' }]} />\n```\n\n## `dir`\n\nRight-to-left layout. Flips the popup alignment, text direction, and\nicon sides so the field reads naturally in Arabic / Hebrew / RTL\nlocales.\n\n```tsx\n<Select dir=\"rtl\" options={…} />\n```\n\n## `aria-label`\n\nAccessibility label for the trigger when there's no visible label\nelsewhere. Forwarded to the `combobox` role.\n\n```tsx\n<Select aria-label=\"Country\" options={…} />\n```\n\n## Performance — `virtualized`, `itemHeight`, `maxMenuHeight`, `pageSize`\n\nKnobs for tuning the popup with large datasets:\n\n- `virtualized` — render only the visible window of options. Auto-on for lists > 100 items; pass `false` to force off, `true` to force on.\n- `itemHeight` — fixed pixel height per row. Default 36. Required input for the virtualization math; bump it if you render taller custom rows via `renderOption`.\n- `maxMenuHeight` — popup height in px before it scrolls. Default 320.\n- `pageSize` — async-only; how many items to fetch per `loadOptions` call. Default 50.\n\n```tsx\n<Select\n    virtualized\n    itemHeight={48}\n    maxMenuHeight={400}\n    pageSize={100}\n    loadOptions={fetchPage}\n/>\n```\n\n## `sortByLocale`\n\nWhen `locale` is set the picker auto-sorts via `Intl.Collator`. Pass\n`sortByLocale={false}` to keep your incoming option order while still\nexposing the locale to assistive tech and to per-row formatting.\n\n```tsx\n<Select locale=\"de\" sortByLocale={false} options={…} />\n```\n\n## Field integration and accessibility props\n\nWrap `Select` in `<Field>` for a label, description, and error message with\nautomatic ARIA wiring. The following attributes are injected onto the trigger\nautomatically by `Field.Control`:\n\n- `id` — matches the `htmlFor` of the label generated by `Field`.\n- `aria-labelledby` — references the `Field.Label` element's ID.\n- `aria-describedby` — references `Field.Description` and/or `Field.Error` when present.\n- `aria-invalid` — set when the enclosing `Field` has a non-null `error`.\n- `aria-required` — set when `Field` has `required={true}`.\n\nYou can also pass any of these props explicitly when using `Select` without a\n`Field` wrapper.\n\n## Props\n\n<PropsTable component=\"Select\" />\n"
}
