Select Primitive
Primitive Select combines Popup and FocusManager with select-specific value context.
Usage
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
useSelect,
} from '@suis-ui/primitives';The recommended primitive structure composes trigger, value, content, and item as compound components.
Select
├── SelectTrigger
│ └── SelectValue
└── SelectContent
└── SelectItemimport { createSignal } from 'solid-js';
import { Select } from '@suis-ui/primitives';
const [value, setValue] = createSignal<string | null>(null);
<Select value={value()} onChangeValue={setValue} placement="bottom-start">
<Select.Trigger>
<Select.Value>
{(value) => value ?? 'Choose an option'}
</Select.Value>
</Select.Trigger>
<Select.Content>
<Select.Item value="small">Small</Select.Item>
<Select.Item value="large">Large</Select.Item>
</Select.Content>
</Select>Props
Value
| Name | Type | Default | Description |
|---|---|---|---|
value | string | null | null | Current selected value. If required is the literal true, null is not allowed. |
onChangeValue | (value: string | null) => void | - | Called when the context value changes. If required is the literal true, null is removed from the callback type. |
required | boolean | false | Prevents clearing the selected item by selecting it again and marks the trigger/listbox as required. |
children | JSX.Element | Required | Select composition. |
Passing required as the literal true removes null from the root value and onChangeValue types. Passing a dynamic boolean such as required={someBoolean} keeps the safer nullable type. Select.Value and the useSelect context value remain string | null because they can represent the uncontrolled initial state.
Popup State And Positioning
Select is built on Popup, so it also accepts Popup state and positioning props.
| Name | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Popup open request value. |
placement | Placement | Floating UI default | Preferred placement. |
strategy | Strategy | Floating UI default | CSS positioning strategy. |
offset | OffsetOptions | - | Enables offset middleware. |
shift | ShiftOptions | boolean | - | Enables shift middleware. |
flip | FlipOptions | boolean | - | Enables flip middleware. |
autoUpdate | AutoUpdateOptions | boolean | true | Recomputes position when layout changes. |
middleware | Middleware[] | - | Additional Floating UI middleware. |
Component
Select.Trigger
Renders a polymorphic trigger with default button and sets role="combobox". It uses Popup trigger behavior and sets aria-required="true" when the root Select is required.
| Name | Type | Default | Description |
|---|---|---|---|
as | T | button | Element or component rendered as the trigger. |
children | JSX.Element | - | Content rendered inside the trigger. |
| Selected element props | Omit<ComponentProps<T>, 'children'> | - | Props forwarded to the trigger element. |
Select.Value
Receives the current value through a render function.
| Name | Type | Default | Description |
|---|---|---|---|
children | (value: string | null) => JSX.Element | Required | Receives the current value and returns content displayed inside the trigger. |
<Select.Value>
{(value) => value ?? 'Placeholder'}
</Select.Value>Select.Content
Renders the listbox in a popup portal. The default element is ul, it sets role="listbox", and it installs focus behavior while the popup is open. It sets aria-required="true" when the root Select is required.
| Name | Type | Default | Description |
|---|---|---|---|
as | T | ul | Element or component rendered as the content. |
children | JSX.Element | - | Option items. |
| Selected element props | Omit<ComponentProps<T>, 'children' | 'style'> | - | Props forwarded to the content element. They are merged with computed popup style. |
Select.Item
Renders an option with default li.
| Name | Type | Default | Description |
|---|---|---|---|
value | string | Required | Value written to Select context when clicked. Selecting the current value again clears it unless required is true. |
as | T | li | Element or component rendered as the item. |
children | JSX.Element | - | Item label or custom content. |
| Selected element props | Omit<ComponentProps<T>, 'value' | 'children'> | - | Props forwarded to the item element. |
Each item receives role="option", data-value, aria-selected, and tabindex={-1}. Built-in items close the popup after click.
Hooks
useSelect
Use useSelect under a Select provider to read the selected value and popup state, or to update value/open state from custom items and supporting controls.
Signature
const [context, actions] = useSelect();const [context, actions]: readonly [
{
value: string | null;
required: boolean;
anchor: Element | null;
element: HTMLElement | null;
position: ComputePositionReturn | null;
open: boolean;
mount: boolean;
},
{
setValue: (value: string | null) => void;
requestOpen: (open: boolean) => void;
},
] = useSelect();Calling it outside a Select provider fails because there is no context to read.
Context
context merges Select value context and Popup context.
| Name | Type | Description |
|---|---|---|
context.value | string | null | Current selected value. It updates when the value prop changes or when setValue is called. |
context.required | boolean | Whether selected items must stay selected when clicked again. |
context.anchor | Element | null | Popup anchor registered by the Select trigger. |
context.element | HTMLElement | null | Listbox element rendered by Select.Content in the portal. |
context.position | ComputePositionReturn | null | Popup position computed by Floating UI. |
context.open | boolean | Most recently requested popup open state. |
context.mount | boolean | Whether content is actually rendered in the portal. |
Custom Select code should use only the value and popup state fields listed above.
Actions
| Name | Type | Description |
|---|---|---|
actions.setValue | (value: string | null) => void | Updates the Select value. Root Select calls onChangeValue through its value-change effect. |
actions.requestOpen | (open: boolean) => void | Requests the Select popup open state. |
Behavior
setValue only updates the value; it does not close the popup automatically. If selection should also close the content, call requestOpen(false) after setValue(value). This action is a low-level setter and does not enforce required by itself. If a custom item supports clearing, check context.required and apply the same policy as built-in Select.Item.
When root Select receives a value prop, an effect syncs context value from that prop. When context value changes, onChangeValue is called, so controlled usage must update the external signal as well.
requestOpen is the Popup action. Select popup positioning, open, mount, and async controller behavior are the same as Popup.
Example
const CustomItem = (props: { value: string; children: JSX.Element }) => {
const [, { setValue, requestOpen }] = useSelect();
return (
<button
type="button"
onClick={() => {
setValue(props.value);
requestOpen(false);
}}
>
{props.children}
</button>
);
};Selection Behavior
Built-in Select.Item always closes the popup after click. The value change depends on the current value and required.
| Case | Result |
|---|---|
| Clicking a different item | Updates to that item value and closes the popup. |
Clicking the selected item when required is false | Changes the value to null and closes the popup. |
Clicking the selected item when required is true | Keeps the value and only closes the popup. |
When the root Select is required, onChangeValue does not receive null even if the context value is changed to null.
Examples
Basic Select
<Select placement="bottom-start">
<Select.Trigger>
<Select.Value>
{(value) => value ?? 'Choose size'}
</Select.Value>
</Select.Trigger>
<Select.Content>
<Select.Item value="small">Small</Select.Item>
<Select.Item value="medium">Medium</Select.Item>
<Select.Item value="large">Large</Select.Item>
</Select.Content>
</Select>Controlled Value
import { createSignal } from 'solid-js';
const [value, setValue] = createSignal<string | null>('medium');
<Select value={value()} onChangeValue={setValue}>
<Select.Trigger>
<Select.Value>
{(value) => value ?? 'Choose size'}
</Select.Value>
</Select.Trigger>
<Select.Content>
<Select.Item value="small">Small</Select.Item>
<Select.Item value="medium">Medium</Select.Item>
<Select.Item value="large">Large</Select.Item>
</Select.Content>
</Select>Required Select
import { createSignal } from 'solid-js';
const [value, setValue] = createSignal('medium');
<Select required value={value()} onChangeValue={setValue}>
<Select.Trigger>
<Select.Value>
{(value) => value ?? 'Choose size'}
</Select.Value>
</Select.Trigger>
<Select.Content>
<Select.Item value="small">Small</Select.Item>
<Select.Item value="medium">Medium</Select.Item>
<Select.Item value="large">Large</Select.Item>
</Select.Content>
</Select>Polymorphic Parts
<Select offset={6} shift flip>
<Select.Trigger as="button" type="button">
<Select.Value>
{(value) => value ?? 'Choose status'}
</Select.Value>
</Select.Trigger>
<Select.Content as="div">
<Select.Item as="button" type="button" value="open">
Open
</Select.Item>
<Select.Item as="button" type="button" value="closed">
Closed
</Select.Item>
</Select.Content>
</Select>