FSAI Design System
Components

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:

  • Select for standard choose-from-list fields
  • AutocompleteSelect for 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

ComponentRole
PickerBase primitive: open state, keyboard navigation, positioning, selection, compound API
SelectPurposed field component built on Picker.Button + Picker.Content
AutocompleteSelectPurposed 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

PartPurpose
PickerRoot component. Handles state, focus, selection, and positioning.
Picker.ButtonStandard button trigger styled like a select field.
Picker.TriggerSlot for a fully custom trigger element.
Picker.InputInput-like trigger used in editable/search-driven flows.
Picker.ContentFloating listbox panel.
Picker.Content.SearchSearch input rendered inside the dropdown.
Picker.Content.HeadingSection heading inside grouped lists.
Picker.ItemSelectable option row.
Picker.EmptyEmpty state container.
Picker.Empty.IconEmpty state icon.
Picker.Empty.TitleEmpty state title.
Picker.Empty.DescriptionEmpty state description.
Picker.Empty.ActionOptional empty state action button.
Picker.LoadingLoading state block inside the content panel.

Root Props

PropTypeDefaultDescription
valueT | null or T[]Current selected value or values.
onChange(value) => voidRequired. Called when selection changes.
multiplebooleanfalseEnables multi-select mode.
openbooleanControlled open state.
defaultOpenbooleanfalseInitial uncontrolled open state.
onOpenChange(open: boolean) => voidCalled when open state changes.
placementPlacement'bottom-start'Floating panel placement.
offsetnumber6Space between trigger and content.
size'md' | 'sm''md'Shared size for trigger/content/item defaults.
variant'outline' | 'filled''outline'Shared trigger/input styling.
closeOnSelectbooleantrueKeeps content open when false. Useful for multi-select or custom flows.
focusStrategy'content' | 'trigger' | 'none''content'Controls where focus goes when opening.
disabledbooleanfalseDisables interaction.

Value Model

Picker supports:

  • primitive values: string, number, boolean
  • object values with a required stable id
Allowed value shapeExample
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:

PatternPathNotes
Toolbar filter pickerfsai/apps/brand-dashboard/src/pages/Home/ChatOverview/ChatOverview.tsxUses Picker.Trigger with an IconButton for multi-select chat filters.
Inline assignment pickerfsai/apps/brand-dashboard/src/modules/locations/components/LocationPanel/LocationPanelDetails/LocationPanelDetails.tsxUses a custom trigger with AvatarGroup for assigned-agent multi-select.

Guidelines

  • Do treat Picker as a primitive, not the default form-field choice
  • Do use object values with a stable id
  • Do use Picker.Trigger for custom triggers and Picker.Button for select-like triggers
  • Do use closeOnSelect={false} for multi-select or iterative selection flows
  • Don't build a standard form dropdown with raw Picker if Select already fits
  • Don't use Picker directly when the main UX is type-to-filter search; use AutocompleteSelect

On this page