Skip to content

Select Primitive

Primitive Select combines Popup and FocusManager with select-specific value context.

Usage

tsx
import {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
  useSelect,
} from '@suis-ui/primitives';

The recommended primitive structure composes trigger, value, content, and item as compound components.

text
Select
├── SelectTrigger
│   └── SelectValue
└── SelectContent
    └── SelectItem
tsx
import { 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

NameTypeDefaultDescription
valuestring | nullnullCurrent selected value.
onChangeValue(value: string | null) => void-Called when the context value changes.
childrenJSX.ElementRequiredSelect composition.

Select is built on Popup, so it also accepts Popup state and positioning props.

NameTypeDefaultDescription
openbooleanfalsePopup open request value.
placementPlacementFloating UI defaultPreferred placement.
strategyStrategyFloating UI defaultCSS positioning strategy.
offsetOffsetOptions-Enables offset middleware.
shiftShiftOptions | boolean-Enables shift middleware.
flipFlipOptions | boolean-Enables flip middleware.
autoUpdateAutoUpdateOptions | booleantrueRecomputes position when layout changes.
middlewareMiddleware[]-Additional Floating UI middleware.

Component

Select.Trigger

Renders a polymorphic trigger with default button and sets role="combobox". It uses Popup trigger behavior.

NameTypeDefaultDescription
asTbuttonElement or component rendered as the trigger.
childrenJSX.Element-Content rendered inside the trigger.
Selected element propsOmit<ComponentProps<T>, 'children'>-Props forwarded to the trigger element.

Select.Value

Receives the current value through a render function.

NameTypeDefaultDescription
children(value: string | null) => JSX.ElementRequiredReceives the current value and returns content displayed inside the trigger.
tsx
<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.

NameTypeDefaultDescription
asTulElement or component rendered as the content.
childrenJSX.Element-Option items.
Selected element propsOmit<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.

NameTypeDefaultDescription
valuestringRequiredValue written to Select context when clicked.
asTliElement or component rendered as the item.
childrenJSX.Element-Item label or custom content.
Selected element propsOmit<ComponentProps<T>, 'value' | 'children'>-Props forwarded to the item element.

Each item receives role="option", data-value, aria-selected, and tabindex={-1}.

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

ts
const [context, actions] = useSelect();
ts
const [context, actions]: readonly [
  {
    value: string | null;
    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.

NameTypeDescription
context.valuestring | nullCurrent selected value. It updates when the value prop changes or when setValue is called.
context.anchorElement | nullPopup anchor registered by the Select trigger.
context.elementHTMLElement | nullListbox element rendered by Select.Content in the portal.
context.positionComputePositionReturn | nullPopup position computed by Floating UI.
context.openbooleanMost recently requested popup open state.
context.mountbooleanWhether content is actually rendered in the portal.

Custom Select code should use only the value and popup state fields listed above.

Actions

NameTypeDescription
actions.setValue(value: string | null) => voidUpdates the Select value. Root Select calls onChangeValue through its value-change effect.
actions.requestOpen(open: boolean) => voidRequests 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).

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

tsx
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>
  );
};

Examples

Basic Select

tsx
<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

tsx
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>

Polymorphic Parts

tsx
<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>