Picker
Low-level floating selection primitive that powers Select, AutocompleteSelect, and custom chooser UIs.
Overview
Picker is the base selection primitive behind several higher-level controls in @fsai/shared-ui.
Use Picker when you need custom selection UX that the higher-level components do not model well. For standard form fields, prefer:
Selectfor standard choose-from-list fieldsAutocompleteSelectfor type-to-filter and async search experiences
Picker is the right choice when you need custom triggers, grouped content, multi-select, nonstandard option rendering, or picker-like UI outside a normal form field.
Layering
| Component | Role |
|---|---|
Picker | Base primitive: open state, keyboard navigation, positioning, selection, compound API |
Select | Purposed field component built on Picker.Button + Picker.Content |
AutocompleteSelect | Purposed searchable field built on Picker.Trigger, Picker.Input, and Picker.Content |
What Picker Handles
Picker is responsible for:
- controlled and uncontrolled open state
- single and multi selection
- Floating UI positioning (
placement,offset, flipping, shifting, max height) - keyboard navigation and typeahead
- focus strategy
- listbox semantics
- empty and loading states
Basic Usage
import { Picker } from "@fsai/shared-ui";
<Picker value={selectedStatus} onChange={setSelectedStatus}>
<Picker.Button placeholder="Select status" />
<Picker.Content>
<Picker.Item value="draft">Draft</Picker.Item>
<Picker.Item value="scheduled">Scheduled</Picker.Item>
<Picker.Item value="published">Published</Picker.Item>
</Picker.Content>
</Picker>;With Custom Trigger
Use Picker.Trigger when the trigger should not look like a standard select button.
<Picker multiple value={selectedAgentIds} onChange={setSelectedAgentIds}>
<Picker.Trigger>
<button type="button" className="rounded-lg px-2 py-1">
{selectedAgents.length > 0 ? (
<AvatarGroup avatars={selectedAgents} avatarSize={24} />
) : (
<span>None</span>
)}
</button>
</Picker.Trigger>
<Picker.Content>
{agents.map((agent) => (
<Picker.Item key={agent.id} value={agent.id}>
<div className="flex items-center gap-1.5">
<Avatar user={agent} className="h-5 w-5" />
<span>
{agent.firstName} {agent.lastName}
</span>
</div>
</Picker.Item>
))}
</Picker.Content>
</Picker>This is the same overall pattern used in LocationPanelDetails for assigned agents.
Inline Action Or Filter Picker
Use Picker directly when the trigger is a compact action control rather than a full form field.
<Picker multiple value={selectedTags} onChange={setSelectedTags}>
<Picker.Trigger>
<IconButton variant="secondary" icon="Filter02" label="Filter Chats" />
</Picker.Trigger>
<Picker.Content>
{chatTags.map((tag) => (
<Picker.Item key={tag} value={tag}>
<div className="flex items-center gap-2">
<TagIcon tag={tag} />
<span>{getConversationTagLabel(tag)}</span>
</div>
</Picker.Item>
))}
</Picker.Content>
</Picker>This is the same pattern used in ChatOverview for chat filtering.
With Search Inside Content
Use Picker.Content.Search when search belongs inside the dropdown panel instead of becoming the main trigger interaction.
<Picker value={selectedTag} onChange={setSelectedTag}>
<Picker.Button placeholder="Select segment" />
<Picker.Content>
<Picker.Content.Search
value={query}
onChange={setQuery}
placeholder="Search"
/>
{filteredTags.map((tag) => (
<Picker.Item key={tag.id} value={tag}>
{tag.name}
</Picker.Item>
))}
</Picker.Content>
</Picker>Use AutocompleteSelect instead when typing and filtering are the primary interaction.
Multi-Select
<Picker multiple value={selectedStates} onChange={setSelectedStates}>
<Picker.Button placeholder="Select states" />
<Picker.Content>
{states.map((state) => (
<Picker.Item key={state.code} value={state}>
{state.name}
</Picker.Item>
))}
</Picker.Content>
</Picker>Picker multi-select works especially well for toolbar filters, inline assignment controls, and compact chooser UIs.
Compound API
| Part | Purpose |
|---|---|
Picker | Root component. Handles state, focus, selection, and positioning. |
Picker.Button | Standard button trigger styled like a select field. |
Picker.Trigger | Slot for a fully custom trigger element. |
Picker.Input | Input-like trigger used in editable/search-driven flows. |
Picker.Content | Floating listbox panel. |
Picker.Content.Search | Search input rendered inside the dropdown. |
Picker.Content.Heading | Section heading inside grouped lists. |
Picker.Item | Selectable option row. |
Picker.Empty | Empty state container. |
Picker.Empty.Icon | Empty state icon. |
Picker.Empty.Title | Empty state title. |
Picker.Empty.Description | Empty state description. |
Picker.Empty.Action | Optional empty state action button. |
Picker.Loading | Loading state block inside the content panel. |
Root Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | T | null or T[] | — | Current selected value or values. |
onChange | (value) => void | — | Required. Called when selection changes. |
multiple | boolean | false | Enables multi-select mode. |
open | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Initial uncontrolled open state. |
onOpenChange | (open: boolean) => void | — | Called when open state changes. |
placement | Placement | 'bottom-start' | Floating panel placement. |
offset | number | 6 | Space between trigger and content. |
size | 'md' | 'sm' | 'md' | Shared size for trigger/content/item defaults. |
variant | 'outline' | 'filled' | 'outline' | Shared trigger/input styling. |
closeOnSelect | boolean | true | Keeps content open when false. Useful for multi-select or custom flows. |
focusStrategy | 'content' | 'trigger' | 'none' | 'content' | Controls where focus goes when opening. |
disabled | boolean | false | Disables interaction. |
Value Model
Picker supports:
- primitive values:
string,number,boolean - object values with a required stable
id
| Allowed value shape | Example |
|---|---|
| primitive | 'draft' |
object with id | { id: 'user_1', name: 'Jane Doe' } |
Object values must expose a stable id. Selection matching is identity-based through id, not deep object equality.
When To Use Picker Directly
Use raw Picker when:
- the trigger is not a normal select field
- you need grouped sections with custom headings
- you need custom item rows or rich option content
- you need multi-select behavior that does not map neatly to
Select - you are building a chooser in a toolbar, panel, or command-like UI
Do not use raw Picker when a standard form field is all you need. In those cases, use Select or AutocompleteSelect.
Brand Dashboard Usage
Representative custom-picker usages:
| Pattern | Path | Notes |
|---|---|---|
| Toolbar filter picker | fsai/apps/brand-dashboard/src/pages/Home/ChatOverview/ChatOverview.tsx | Uses Picker.Trigger with an IconButton for multi-select chat filters. |
| Inline assignment picker | fsai/apps/brand-dashboard/src/modules/locations/components/LocationPanel/LocationPanelDetails/LocationPanelDetails.tsx | Uses a custom trigger with AvatarGroup for assigned-agent multi-select. |
Guidelines
- Do treat
Pickeras a primitive, not the default form-field choice - Do use object values with a stable
id - Do use
Picker.Triggerfor custom triggers andPicker.Buttonfor select-like triggers - Do use
closeOnSelect={false}for multi-select or iterative selection flows - Don't build a standard form dropdown with raw
PickerifSelectalready fits - Don't use
Pickerdirectly when the main UX is type-to-filter search; useAutocompleteSelect