# Select

A cross-platform React select component for choosing from a list of predefined options with dropdown menu. Works on both React (web) and React Native.
<!-- BEGIN:xui-mcp-instructions:select -->
A single-value dropdown input that displays the current selection and opens a ContextMenu panel when clicked. Supports five sizes, an optional leading icon, a clear button, and an icon-only mode. The dropdown panel is powered by ContextMenu — see its documentation for option list composition, search, and behaviour.

### When to use
- When the user must pick exactly one value from a fixed, mutually exclusive list — country, currency, status, category
- When the list of options is known, finite, and does not require typing to navigate
- When vertical space is limited and showing every option (as with radio buttons) would be too large
- When the list is too long to show inline — use Select instead of radio buttons when there are more than 5–6 options
- In forms, filter bars, and settings panels where a compact single-line field is needed
- As part of a larger form alongside other controls

### When not to use
- When the user can choose more than one value — use Multiselect instead
- When the list is long or the user benefits from typing to filter — use Autocomplete / Combobox instead
- When the value is freeform text the user must type — use TextArea or Input instead
- When there are only 2–3 options that should always stay visible — use Radio buttons or ToggleButtonGroup instead
- When the choice is a simple on/off — use a Switch or Checkbox instead
- When the value is time or date — use InputTime or DatePicker instead

### Content guidelines
- Label text (placeholder) — use a concise instructional phrase: *"Select language"*, *"Choose currency"*, *"Pick a category"*. Avoid generic placeholders like *"Select…"* alone when a more specific hint is possible.
- Option labels — label each option clearly and concisely — one short phrase, no trailing punctuation. Use parallel structure across all options in the same list (all nouns, or all short phrases). Avoid negative options (e.g. *"Do not notify me"*) — rephrase positively.
- Option order — list options in a logical order: most common first, alphabetical, or by value. Never randomise.
- Option descriptions — use the ContextMenu Description slot to add clarifying context when the label alone is not sufficient — e.g. label *"Admin"*, description *"Full access to all settings"*.
- Selected value display — show the full option label as selected. If the label is very long and the field is narrow, truncate with ellipsis and show the full label in a tooltip on hover.
- Field label — always provide a visible label above the Select via FieldGroup. Do not rely on the placeholder text alone as the field identifier.
- Error messages — be specific:
- *"Please select a language"* (required, empty on submit)
- *"Selected option is no longer available"* (stale value)

Icon choice — use a left icon that represents the category being selected, not a generic dropdown indicator. The chevron already signals *"this opens a list."*

### Behaviour guidelines
- Single selection — only one value can be selected at a time. Selecting a new option automatically replaces the previous selection. The value cannot be *"unselected"* by clicking it again — to clear the selection, provide an explicit clear button (Clear button=true) or a *"None"* / *"No selection"* option in the list.
- Opening — clicking the field or pressing Enter / Space / ↓ when focused opens the ContextMenu. The field transitions to State=Focus; the chevron rotates to point upward.
- Closing — the ContextMenu closes on: selecting an option, pressing Escape, clicking outside, or pressing Tab past the last option. On close, focus returns to the Select field.
- Selection — when the user selects an option from the ContextMenu, the field updates to Filled=True and the label shows the selected option's text. The dropdown closes immediately.
- Clear — clicking the clear button (✕) clears the selection (Filled=False), closes the dropdown if open, and returns focus to the field.
- Validation — validate on blur (when the field loses focus without a selection) or on form submit. Switch to State=Error with a specific message if a required field has no value. Clear the error when a value is selected.
- Placeholder — show placeholder text in Filled=False using Label text. The placeholder should describe what to select (e.g. *"Select a country"*, *"Choose language"*) rather than repeating the field label.
- Disabled — State=Disable prevents opening the dropdown. If a value is present, it remains visible in muted style. Provide a tooltip or nearby explanation if the reason for disabling is not obvious.
- ContextMenu size matching — the ContextMenu panel width should match or exceed the Select field width. Never open a narrower ContextMenu than the trigger field — options would be cut off.

### Accessibility
- Select must render as a <button> (or a <div> with role=*"combobox"*) with aria-haspopup=*"listbox"* and aria-expanded=*"true"* / *"false"* reflecting the open state.
- The field must have an accessible name via <label for> (from FieldGroup), aria-label, or aria-labelledby.
- When Icon only=True, aria-label on the field is mandatory — the label is hidden and the icon alone conveys no text to screen readers.
- When Filled=True, reflect the selected value in aria-label or ensure it is part of the field's visible text that screen readers can read: e.g. aria-label=*"Language: English"*.
- The clear button must have aria-label=*"Clear selection"* or aria-label=*"Clear [field name]"*.
- When State=Error, the error message must be linked via aria-describedby and placed in an aria-live=*"polite"* region.
- Keyboard navigation:
- Enter / Space / ↓ opens the dropdown
- ↑ / ↓ navigate options inside the ContextMenu
- Enter selects the focused option
- Escape closes without selecting; focus returns to the field
- Tab closes and moves to the next focusable element

The left icon must have aria-hidden=*"true"* when a label is also visible.
<!-- END:xui-mcp-instructions:select -->

## Installation

```bash
npm install @xsolla/xui-select
```

## Demo

### Basic Select

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function BasicSelect() {
  const [value, setValue] = React.useState("");

  return (
    <Select
      value={value}
      onChange={setValue}
      options={["Option 1", "Option 2", "Option 3"]}
      placeholder="Select an option"
    />
  );
}
```

### Select with Label

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function LabeledSelect() {
  const [country, setCountry] = React.useState("");

  return (
    <Select
      label="Country"
      value={country}
      onChange={setCountry}
      options={[
        "United States",
        "Canada",
        "United Kingdom",
        "Germany",
        "France",
      ]}
      placeholder="Select a country"
    />
  );
}
```

### Select with Object Options

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function ObjectOptionsSelect() {
  const [status, setStatus] = React.useState("");

  const options = [
    { label: "Active", value: "active" },
    { label: "Pending", value: "pending" },
    { label: "Inactive", value: "inactive" },
  ];

  return (
    <Select
      label="Status"
      value={status}
      onChange={setStatus}
      options={options}
      placeholder="Select status"
    />
  );
}
```

### Select Sizes

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function SelectSizes() {
  const options = ["Small", "Medium", "Large"];

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <Select size="xs" options={options} placeholder="Extra Small" />
      <Select size="sm" options={options} placeholder="Small" />
      <Select size="md" options={options} placeholder="Medium (default)" />
      <Select size="lg" options={options} placeholder="Large" />
      <Select size="xl" options={options} placeholder="Extra Large" />
    </div>
  );
}
```

### Select with Icons

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";
import { Globe } from "@xsolla/xui-icons-base";

export default function SelectWithIcons() {
  return (
    <Select
      iconLeft={<Globe />}
      options={["English", "Spanish", "French", "German"]}
      placeholder="Select language"
    />
  );
}
```

## Anatomy

Import the component and use it directly:

```jsx
import { Select } from "@xsolla/xui-select";

<Select
  label="Field Label" // Optional label above select
  value={value} // Controlled selected value
  onChange={setValue} // Value change handler
  options={["A", "B", "C"]} // Array of options (strings or objects)
  placeholder="Select..." // Placeholder text
  iconLeft={<Icon />} // Optional left icon
  iconRight={<Icon />} // Optional right icon (default: ChevronDown)
  filled // Whether to show filled background
  state="error" // State: "default" | "disable" | "error"
  errorMessage="Error message" // Error message (shown when state="error")
/>;
```

## Examples

### Error State

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function ErrorSelect() {
  return (
    <Select
      label="Required Field"
      state="error"
      errorMessage="Please select an option"
      options={["Option 1", "Option 2", "Option 3"]}
      placeholder="Select an option"
    />
  );
}
```

### Disabled Select

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function DisabledSelect() {
  return (
    <Select
      label="Disabled Field"
      value="Option 1"
      state="disable"
      options={["Option 1", "Option 2", "Option 3"]}
    />
  );
}
```

### Unfilled/Transparent Background

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function UnfilledSelect() {
  return (
    <Select
      filled={false}
      options={["Option 1", "Option 2", "Option 3"]}
      placeholder="Transparent background"
    />
  );
}
```

### Icon Only Select

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";
import { Settings } from "@xsolla/xui-icons";

export default function IconOnlySelect() {
  return (
    <Select
      iconOnly
      iconLeft={<Settings />}
      options={["Settings", "Preferences", "Advanced"]}
    />
  );
}
```

### Full Width Select

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function FullWidthSelect() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <Select
        fullWidth
        options={["Option 1", "Option 2", "Option 3"]}
        placeholder="Stretches to fill container"
      />
      <Select
        fullWidth={false}
        options={["Option 1", "Option 2", "Option 3"]}
        placeholder="Intrinsic width"
      />
    </div>
  );
}
```

### Custom Popover Width

By default the dropdown popover matches the trigger width. Use `popoverWidth` to
size the popover independently — handy when option labels are wider than the
field. Pass a number (pixels) or any CSS width string.

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

export default function CustomPopoverWidth() {
  return (
    <div style={{ width: 200 }}>
      <Select
        popoverWidth={320}
        options={[
          "Account overview and billing details",
          "Subscription management",
          "Payment history and transactions",
        ]}
        placeholder="Custom popover width"
      />
    </div>
  );
}
```

### Form with Select

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";
import { Input } from "@xsolla/xui-input";
import { Button } from "@xsolla/xui-button";

export default function FormWithSelect() {
  const [form, setForm] = React.useState({
    name: "",
    category: "",
    priority: "",
  });

  return (
    <div
      style={{ display: "flex", flexDirection: "column", gap: 16, width: 300 }}
    >
      <Input
        label="Task Name"
        value={form.name}
        onChangeText={(name) => setForm((prev) => ({ ...prev, name }))}
        placeholder="Enter task name"
      />
      <Select
        label="Category"
        value={form.category}
        onChange={(category) => setForm((prev) => ({ ...prev, category }))}
        options={["Development", "Design", "Marketing", "Sales"]}
        placeholder="Select category"
      />
      <Select
        label="Priority"
        value={form.priority}
        onChange={(priority) => setForm((prev) => ({ ...prev, priority }))}
        options={[
          { label: "High", value: "high" },
          { label: "Medium", value: "medium" },
          { label: "Low", value: "low" },
        ]}
        placeholder="Select priority"
      />
      <Button>Create Task</Button>
    </div>
  );
}
```

### Dropdown Positioning

By default the dropdown always opens on the `side` (default `"bottom"`). Pass `autoFlip` to let it flip to the opposite side when there isn't enough viewport space.

```tsx
import * as React from "react";
import { Select } from "@xsolla/xui-select";

const options = ["Option 1", "Option 2", "Option 3"];

// Always opens below (default — no surprises for existing layouts)
<Select options={options} placeholder="Default" />

// Always opens above
<Select side="top" options={options} placeholder="Opens above" />

// Opens below, flips above when near the bottom of the viewport
<Select autoFlip options={options} placeholder="Auto-flip enabled" />

// Opens above, flips below when near the top of the viewport
<Select side="top" autoFlip options={options} placeholder="Prefers top, flips if needed" />
```

## API Reference

### Select

The main select component. Renders a button trigger with dropdown menu.

**Select Props:**

| Prop              | Type                                                      | Default           | Description                                                                                                   |
| :---------------- | :-------------------------------------------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------ |
| `testID`          | `string`                                                  | —                 | Test ID for testing frameworks. On web this renders as `data-testid`; on React Native it renders as `testID`. |
| value             | `string`                                                  | -                 | The controlled selected value.                                                                                |
| placeholder       | `string`                                                  | `"Select"`        | Placeholder text when no value selected.                                                                      |
| onPress           | `() => void`                                              | -                 | Callback when select trigger is pressed.                                                                      |
| size              | `"xl" \| "lg" \| "md" \| "sm" \| "xs"`                    | `"md"`            | The size of the select.                                                                                       |
| state             | `"default" \| "hover" \| "focus" \| "disable" \| "error"` | `"default"`       | The visual state of the select.                                                                               |
| label             | `string`                                                  | -                 | Label text displayed above the select.                                                                        |
| disabled          | `boolean`                                                 | `false`           | Whether the select is disabled. Also triggered when `state="disable"`.                                        |
| errorMessage      | `string`                                                  | -                 | Error message displayed below the select (also sets error styling).                                           |
| iconLeft          | `ReactNode`                                               | -                 | Icon displayed on the left side.                                                                              |
| iconRight         | `ReactNode`                                               | `<ChevronDown />` | Icon displayed on the right side.                                                                             |
| filled            | `boolean`                                                 | `true`            | Whether to show filled background.                                                                            |
| iconOnly          | `boolean`                                                 | `false`           | Whether to show only icon without text.                                                                       |
| options           | `(string \| SelectOption)[]`                              | `[]`              | Array of options to display.                                                                                  |
| onChange          | `(value: string) => void`                                 | -                 | Callback when value changes.                                                                                  |
| searchable        | `boolean`                                                 | `false`           | Whether to show a search input in the dropdown.                                                               |
| searchPlaceholder | `string`                                                  | `"Search"`        | Placeholder text for the search input.                                                                        |
| noOptionsMessage  | `string`                                                  | `"No results"`    | Message shown when no options match the search.                                                               |
| clearable         | `boolean`                                                 | `false`           | Show a clear button to reset the selected value (field variant only).                                         |
| onClear           | `() => void`                                              | -                 | Callback when the clear button is pressed.                                                                    |
| maxHeight         | `number`                                                  | `300`             | Maximum height of the dropdown in pixels.                                                                     |
| popoverWidth      | `number \| string`                                       | `"100%"`          | Width of the dropdown popover/menu. Defaults to the trigger width; pass a number (px) or CSS width string to size it independently of the trigger. |
| fullWidth         | `boolean`                                                 | `true`            | Whether the select should stretch to fill the full width of its container.                                    |
| overlayThemeMode  | `ThemeMode`                                               | `themeMode`       | Theme mode for the dropdown overlay.                                                                          |
| overlayThemeProductContext | `ProductContext`                                 | `themeProductContext` | Product context for the dropdown overlay.                                                                 |
| side              | `"top" \| "bottom"`                                       | `"bottom"`        | Preferred side for the dropdown.                                                                              |
| autoFlip          | `boolean`                                                 | `false`           | When true, the dropdown flips to the opposite side when there isn't enough viewport space on the preferred side. |

**SelectOption Type:**

```typescript
interface SelectOption {
  label: string; // Display text
  value: any; // Value to be selected
  disabled?: boolean; // Whether this option is disabled
}
```

## React Native Notes

The Select component works on React Native with the following differences:

| Feature               | Web                           | React Native                        |
| --------------------- | ----------------------------- | ----------------------------------- |
| Searchable dropdown   | Supported (`searchable` prop) | **Not available** — prop is ignored |
| Dropdown shadow       | CSS `box-shadow`              | Android `elevation: 4`              |
| Scroll behavior       | `overflowY: auto`             | Native `ScrollView`                 |
| Cursor styles         | `pointer` / `not-allowed`     | Ignored                             |
| Click-outside dismiss | `document.addEventListener`   | Not active (guarded by `isNative`)  |

## Accessibility

- Trigger has `role="combobox"`, `aria-haspopup="listbox"`, and `aria-expanded` (updates on open/close)
- Trigger links to its label via `aria-labelledby` when the `label` prop is set
- Dropdown has `role="listbox"`; each option has `role="option"` and `aria-selected`
- Disabled trigger and disabled options expose `aria-disabled="true"`
- Search input has `aria-label` matching `searchPlaceholder`
- Selected item auto-scrolls into view when the dropdown opens (web only)
- Disabled state properly communicated to assistive technology
