Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Combobox

Autocomplete input with a list of suggestions, built on @base-ui/react.

Playground

—showTrigger
—showClear
—autoHighlight
—aria-invalid
—disabled
Usage
Combobox owns the items, filtering, and selection state — pass the options through items. ComboboxInput renders an InputGroup with a caret trigger, ComboboxContent portals the popup sized to the anchor, and ComboboxList takes a render function called per matching item. ComboboxEmpty shows when the filter clears everything out.
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@workspace/ui/components/combobox"

const frameworks = ["Next.js", "SvelteKit", "Nuxt", "Remix", "Astro"]

<Combobox items={frameworks}>
  <ComboboxInput placeholder="Select a framework" />
  <ComboboxContent>
    <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
Clear button
showClear on ComboboxInput adds a clear button once a value is selected — it replaces the caret trigger while a selection exists. Set defaultValue for an initial selection, or control it with value / onValueChange on the root.
<Combobox items={frameworks} defaultValue={frameworks[0]}>
  <ComboboxInput placeholder="Select a framework" showClear />
  <ComboboxContent>
    <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
Groups
For grouped options, pass { value, items } objects to the root and render each group as a ComboboxGroup — a ComboboxLabel for the heading and a ComboboxCollection with its own render function for the nested items. ComboboxSeparator divides groups; it hides automatically while filtering.
const timezones = [
  {
    value: "Americas",
    items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-3) São Paulo"],
  },
  {
    value: "Europe",
    items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"],
  },
  // ...
]

<Combobox items={timezones}>
  <ComboboxInput placeholder="Select a timezone" />
  <ComboboxContent>
    <ComboboxEmpty>No timezones found.</ComboboxEmpty>
    <ComboboxList>
      {(group, index) => (
        <ComboboxGroup key={group.value} items={group.items}>
          <ComboboxLabel>{group.value}</ComboboxLabel>
          <ComboboxCollection>
            {(item) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxCollection>
          {index < timezones.length - 1 && <ComboboxSeparator />}
        </ComboboxGroup>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
Multiple
multiple turns the value into an array and keeps the popup open across selections. Selected values render as removable ComboboxChips with a ComboboxChipsInput for typing. The chips container replaces ComboboxInput as the anchor — create a ref with useComboboxAnchor(), attach it to ComboboxChips, and pass it to ComboboxContent. Backspace in the empty input removes the last chip.
Next.js
const anchor = useComboboxAnchor()

<Combobox multiple autoHighlight items={frameworks} defaultValue={[frameworks[0]]}>
  <ComboboxChips ref={anchor} className="w-full max-w-72">
    <ComboboxValue>
      {(values: string[]) => (
        <React.Fragment>
          {values.map((value) => (
            <ComboboxChip key={value}>{value}</ComboboxChip>
          ))}
          <ComboboxChipsInput placeholder="Add framework" />
        </React.Fragment>
      )}
    </ComboboxValue>
  </ComboboxChips>
  <ComboboxContent anchor={anchor}>
    <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
Popup
To launch the combobox from a button, render a ComboboxTrigger via the render prop and show the selection with ComboboxValue — its placeholder fills in while nothing is selected. Move ComboboxInput inside ComboboxContent with showTrigger={false} so the search field lives in the popup, Select-style.
<Combobox items={frameworks}>
  <ComboboxTrigger
    render={<Button variant="outline" className="w-56 justify-between font-normal" />}
  >
    <ComboboxValue placeholder="Select a framework" />
  </ComboboxTrigger>
  <ComboboxContent>
    <ComboboxInput showTrigger={false} placeholder="Search frameworks..." />
    <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
Custom items
Items can be objects — provide itemToStringValue so filtering and the input display work against a string, and render anything inside ComboboxItem. Here each option is an Item with a title and description; the check indicator still pins to the end edge.
type Teammate = { value: string; label: string; role: string }

const teammates: Teammate[] = [
  { value: "ana", label: "Ana Souza", role: "Product Designer" },
  { value: "bruno", label: "Bruno Lima", role: "Frontend Engineer" },
  // ...
]

<Combobox
  items={teammates}
  itemToStringValue={(teammate) => teammate.label}
>
  <ComboboxInput placeholder="Assign a teammate" />
  <ComboboxContent>
    <ComboboxEmpty>No teammates found.</ComboboxEmpty>
    <ComboboxList>
      {(teammate) => (
        <ComboboxItem key={teammate.value} value={teammate}>
          <Item size="xs" className="p-0">
            <ItemContent>
              <ItemTitle>{teammate.label}</ItemTitle>
              <ItemDescription>{teammate.role}</ItemDescription>
            </ItemContent>
          </Item>
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
RTL
The input, trigger, chips, and item indicator all use logical properties, so they mirror under a dir="rtl" ancestor. The popup is portalled out of that ancestor — pass dir="rtl" to ComboboxContent as well so the list mirrors with it.
<div dir="rtl">
  <Combobox items={cities}>
    <ComboboxInput placeholder="اختر مدينة" />
    <ComboboxContent dir="rtl">
      <ComboboxEmpty>لم يتم العثور على نتائج.</ComboboxEmpty>
      <ComboboxList>
        {(item) => (
          <ComboboxItem key={item} value={item}>
            {item}
          </ComboboxItem>
        )}
      </ComboboxList>
    </ComboboxContent>
  </Combobox>
</div>
Accessibility
Base UI implements the WAI-ARIA combobox pattern: the input is role="combobox" with aria-expanded, the popup list is role="listbox", and each option is role="option" with aria-selected. Arrow keys move the highlight, Enter selects, and Escape closes the popup. Give the input an accessible name by pairing it with a Field label or an aria-label.
<Field>
  <FieldLabel htmlFor="framework">Framework</FieldLabel>
  <Combobox items={frameworks}>
    <ComboboxInput id="framework" placeholder="Select a framework" />
    {/* ... */}
  </Combobox>
</Field>
API Reference
Sixteen parts wrap the Base UI Combobox: Combobox as the stateful root, ComboboxInput (an InputGroup with built-in trigger and clear buttons), ComboboxContent (portal, positioner, and popup in one), ComboboxList, ComboboxItem, ComboboxEmpty, plus group parts (ComboboxGroup, ComboboxLabel, ComboboxCollection, ComboboxSeparator), chips parts (ComboboxChips, ComboboxChip, ComboboxChipsInput), trigger parts (ComboboxTrigger, ComboboxValue), and the useComboboxAnchor helper. All parts accept their underlying Base UI props.
ComponentPropDefault
Comboboxitems?: T[], value?, defaultValue?, onValueChange?, itemToStringValue?: (item: T) => string—
Comboboxmultiple?: boolean, autoHighlight?: boolean, openOnInputClick?: boolean, filter?, limit?: numberfalse / false / true
ComboboxInputshowTrigger?: boolean, showClear?: boolean, disabled?: booleantrue / false / false
ComboboxContentside?, align?, sideOffset?, alignOffset?, anchor?bottom / start / 6 / 0
ComboboxValueplaceholder?: ReactNode, children?: ReactNode | (value) => ReactNode—
ComboboxGroupitems?: T[]—
ComboboxChipshowRemove?: booleantrue
<ComboboxValue data-slot="combobox-value" />
<ComboboxTrigger data-slot="combobox-trigger" />
<ComboboxClear data-slot="combobox-clear" />
<ComboboxContent data-slot="combobox-content" />
<ComboboxList data-slot="combobox-list" />
<ComboboxItem data-slot="combobox-item" />
<ComboboxGroup data-slot="combobox-group" />
<ComboboxLabel data-slot="combobox-label" />
<ComboboxCollection data-slot="combobox-collection" />
<ComboboxEmpty data-slot="combobox-empty" />
<ComboboxSeparator data-slot="combobox-separator" />
<ComboboxChips data-slot="combobox-chips" />
<ComboboxChip data-slot="combobox-chip" />
<ComboboxChipsInput data-slot="combobox-chip-input" />

Tokens used

16 design tokens consumed by the Combobox component.

  • --popover

    Popover

    bg-popover
  • --popover-foreground

    Popover Foreground

    text-popover-foreground
  • --foreground

    Foreground

    text-foregroundring-foreground/10
  • --accent

    Accent

    data-highlighted:bg-accent
  • --accent-foreground

    Accent Foreground

    data-highlighted:text-accent-foreground
  • --muted

    Muted

    bg-muted
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --input

    Input

    border-inputborder-input/30bg-input/30
  • --border

    Border

    bg-border
  • --ring

    Ring

    focus-within:border-ringfocus-within:ring-ring/50
  • --destructive

    Destructive

    has-aria-invalid:border-destructivehas-aria-invalid:ring-destructive/20
  • --text-sm

    Text Small (14px)

    text-sm
  • --text-xs

    Text Extra Small (12px)

    text-xs
  • --radius-sm

    Radius 0.6× (6px)

    rounded-sm
  • --radius-md

    Radius 0.8× (8px)

    rounded-md
  • --spacing

    Spacing unit (4px)

    p-1py-1.5ps-2pe-8gap-2size-4min-h-9