ComplexSelector
Use ComplexSelector when a selection needs richer custom content than a Selector option row. It is intentionally one component: ComplexSelector owns the field, trigger, popover, focus restore, and changeAction flow, while the content render prop owns the selector-specific accessible structure.
Installation
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0
Requires React 19+, base CSS and a Theme provider. See Setup.
Complex Selector
Use ComplexSelector when a selection needs richer custom content than a Selector option row. It is intentionally one component: ComplexSelector owns the field, trigger, popover, focus restore, and changeAction flow, while the content render prop owns the selector-specific accessible structure.
Usage guidance
- Use variant="secondary" beside secondary buttons, or variant="ghost" beside ghost buttons; add a startIcon when it helps identify the trigger. Use alignment="end" when a wide surface should align its end edge to the trigger.
- For staged editors, keep draft state in the composed content and call the provided onChange helper only from Apply. Cancel or dismiss without committing.
- Compose the dialog content from the appropriate accessible structure for the job: RadioList for a simple choice, Calendar/date inputs for date picking, TreeList or a searchable list for hierarchy, or a custom grid when two-dimensional arrow navigation is useful.
- Use the provided onChange helper from children; it already calls both onChange and changeAction and updates optimistic busy state.
- Call close() from custom content when a selection should dismiss the popup. Keep it open for multi-step content or freeform entry flows.
- Use Astryx focus hooks for custom content: useGridFocus for two-dimensional grids, useTreeFocus through TreeList for hierarchies, and useListFocus for custom linear collections.
- Evaluate custom content against WCAG 2.2: keyboard operation, focus visible/not obscured, names and roles, labels/instructions, target size, and contrast/non-text contrast are especially relevant for selector popovers.
- Avoid: Do not rebuild trigger ARIA, popover focus management, or changeAction handling in product code.
- Avoid: Do not use ComplexSelector for a plain single-column text list; use Selector instead.
Theming API
{
"targets": [
{
"className": "astryx-complex-selector",
"visualProps": [
"variant",
"size",
"status"
]
},
{
"className": "astryx-complex-selector-indicator-icon",
"states": [
"state"
]
},
{
"className": "astryx-complex-selector-popup"
}
]
}
Complex Selector
An input or toolbar trigger and dialog-popover shell for custom selector content.
| Prop | Type | Default | Description |
|---|---|---|---|
| label * | string | Label text for accessibility and the field label. | |
| value * | Value | Current controlled value. | |
| onChange | (value: Value) => void | Called when custom content commits a new value. | |
| changeAction | (value: Value) => void | Promise<void> | Async action after onChange. ComplexSelector exposes optimistic value and busy state while pending. | |
| children * | (value: Value, onChange: (value: Value) => void, close: () => void, state: ComplexSelectorRenderState) => ReactNode | Custom dialog content. Receives positional value, onChange, close, and state helpers. | |
| triggerLabel | ReactNode | Label/content shown in the closed trigger. | |
| placeholder | ReactNode | 'Select...' | Placeholder shown when triggerLabel is omitted. |
| isDisabled | boolean | Disables the selector. | |
| isLoading | boolean | Shows loading state on the trigger. | |
| status | {type: 'warning' | 'error' | 'success', message?: string} | Validation status. | |
| statusVariant | 'attached' | 'detached' | 'tooltip' | 'attached' for input selectors; 'detached' for secondary and ghost selectors | How the status message is placed relative to the input. attached overlaps directly below the input (bordered treatment; detached when variant is secondary or ghost); detached floats below as a separate element with spacing; tooltip reveals the message in an info-tip button. |
| size | 'sm' | 'md' | 'lg' | 'md' | Exact trigger height: sm 28px, md 32px, or lg 36px. |
| variant | 'input' | 'secondary' | 'ghost' | 'input' | Visual trigger style. Input is the bordered form treatment; secondary matches the secondary buttons of the active theme (borderless and neutral-filled by default); ghost is borderless and transparent to match ghost buttons. |
| startIcon | ReactNode | IconType | Icon displayed at the start of the trigger. | |
| width | SizeValue | Width of the field. | |
| placement | 'above' | 'below' | 'start' | 'end' | 'below' | Popup placement. |
| alignment | 'start' | 'center' | 'end' | 'start' | Popup alignment along the placement axis. |
| renderTrigger | (props: ComplexSelectorRenderTriggerProps) => ReactNode | Render the control the popup hangs off — a glyph in a list row, a chip, an icon button — instead of the selector's own field and button. Spread the given props ({ref, id, onClick, onKeyDown, aria-haspopup, aria-expanded, aria-controls, aria-busy}) onto it; the popup is anchored to that control and still labelled by label. The field chrome is not rendered. Pair with handleRef to open from a keystroke elsewhere. | |
| handleRef | React.Ref<ComplexSelectorHandle> | Imperative handle for programmatic control. Exposes open(), close(), toggle(), and isOpen(). | |
| onOpenChange | (isOpen: boolean) => void | Called whenever the surface opens or closes, however it happened — trigger, keyboard, light dismiss, Escape, close(), or the imperative handle. | |
| contentXstyle | StyleXStyles | StyleX styles for the popup content container. | |
| onClick | (event: MouseEvent) => void | Click handler on the trigger. Runs before the popup opens; call preventDefault() to keep it closed. A press elsewhere in the control box goes through the trigger too. With renderTrigger it runs after the built-in toggle and cannot cancel it. | |
| onKeyDown | (event: KeyboardEvent) => void | Key handler on the trigger. Runs after the built-in handling and receives every key, including the ArrowDown that opens the popup. With renderTrigger it is skipped for keys the built-in handling cancels. | |
| containerXstyle | StyleXStyles | StyleX styles for the field container (label, control, and status together). No effect inside InputGroup (style the InputGroup instead) or in a horizontal-labels FormLayout. Root xstyle, className, and style are not accepted. | |
| containerClassName | string | Class name for the field container. Same targeting as containerXstyle. | |
| containerStyle | CSSProperties | Inline styles for the field container. Same targeting as containerXstyle. | |
| controlXstyle | StyleXStyles | StyleX styles for the visual control box: the element that paints the border and background and holds the value, icons, and end controls. Never the trigger button itself. | |
| controlClassName | string | Class name for the visual control box. | |
| controlStyle | CSSProperties | Inline styles for the visual control box. |
Anatomy
- Field: Field shell that provides the label and optional supporting field content.
- Trigger: Control that displays the current value or placeholder and opens the popup.
- Icon-rendered start icon: Optional leading semantic icon or icon component rendered through Icon.
- Caller-rendered start content: Optional arbitrary React content rendered directly at the start of the trigger.
- Indicator icon: Trailing chevron that rotates to reflect whether the popup is open.
- Popup: Mounted dialog surface that is painted and shown while open and hidden while closed.
Examples
- Complex Selector: A two-axis picker: choose a fruit and a ripeness level from one control. ComplexSelector owns the trigger, popover, and focus restore while the custom grid owns its keyboard semantics.
- Complex Selector — Deadline Picker: A multi-step deadline field: pick a preset like Today or Next week, or switch to a custom date and time before applying. The popup stays open until the user commits, so the content owns the Apply action.
- Complex Selector — Tree Search: A destination picker that combines a search field with a TreeList hierarchy. TreeList owns tree keyboard navigation; ComplexSelector owns the trigger, popover, and focus restore. Selecting a folder closes the popup.
Source StyleX compilation
ComplexSelectorShowcase contain stylex.create and require a StyleX source build pipeline. Precompiled Core CSS alone does not compile these examples. See Styling before copying them.