@xsolla/xui-select
v0.223.1
Published
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 selec
Readme
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.
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 (or a with role="combobox") with aria-haspopup="listbox" and aria-expanded="true" / "false" reflecting the open state.
- The field must have an accessible name via (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.
Installation
npm install @xsolla/xui-selectDemo
Basic Select
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
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
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
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
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:
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
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
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
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
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
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.
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
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.
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" />Scrolling a Long List
The dropdown closes when the page scrolls. On web, the options list contains its
own scroll (overscroll-behavior: contain). When the user scrolls to the end of
the list and keeps scrolling, the page behind it does not scroll, so the
dropdown stays open. No prop is 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:
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 + overscroll-behavior: contain | 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", andaria-expanded(updates on open/close) - Trigger links to its label via
aria-labelledbywhen thelabelprop is set - Dropdown has
role="listbox"; each option hasrole="option"andaria-selected - Disabled trigger and disabled options expose
aria-disabled="true" - Search input has
aria-labelmatchingsearchPlaceholder - Selected item auto-scrolls into view when the dropdown opens (web only)
- Disabled state properly communicated to assistive technology
