Sabil UI

Search documentation

Search for a command to run...

Components

Combobox

Autocomplete input and command palette with a list of suggestions.

Installation#

  1. Install the dependencies

    npm install @base-ui/react cn lucide-react
  2. Copy the source into your project

    components/ui/combobox.tsx
    "use client"import * as React from "react"import { Combobox as ComboboxPrimitive } from "@base-ui/react"import { cn } from "cn"import { Button } from "@/components/ui/button"import {  InputGroup,  InputGroupAddon,  InputGroupButton,  InputGroupInput,} from "@/components/ui/input-group"import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"const Combobox = ComboboxPrimitive.Rootfunction ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {  return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />}function ComboboxTrigger({  className,  children,  ...props}: ComboboxPrimitive.Trigger.Props) {  return (    <ComboboxPrimitive.Trigger      data-slot="combobox-trigger"      className={cn("[&_svg:not([class*='size-'])]:size-4", className)}      {...props}    >      {children}      <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />    </ComboboxPrimitive.Trigger>  )}function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {  return (    <ComboboxPrimitive.Clear      data-slot="combobox-clear"      render={<InputGroupButton variant="ghost" size="icon-xs" />}      className={cn(className)}      {...props}    >      <XIcon className="pointer-events-none" />    </ComboboxPrimitive.Clear>  )}function ComboboxInput({  className,  children,  disabled = false,  showTrigger = true,  showClear = false,  ...props}: ComboboxPrimitive.Input.Props & {  showTrigger?: boolean  showClear?: boolean}) {  return (    <InputGroup className={cn("w-auto", className)}>      <ComboboxPrimitive.Input        render={<InputGroupInput disabled={disabled} />}        {...props}      />      <InputGroupAddon align="inline-end">        {showTrigger && (          <InputGroupButton            size="icon-xs"            variant="ghost"            asChild            data-slot="input-group-button"            className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"            disabled={disabled}          >            <ComboboxTrigger />          </InputGroupButton>        )}        {showClear && <ComboboxClear disabled={disabled} />}      </InputGroupAddon>      {children}    </InputGroup>  )}function ComboboxContent({  className,  side = "bottom",  sideOffset = 6,  align = "start",  alignOffset = 0,  anchor,  ...props}: ComboboxPrimitive.Popup.Props &  Pick<    ComboboxPrimitive.Positioner.Props,    "side" | "align" | "sideOffset" | "alignOffset" | "anchor"  >) {  return (    <ComboboxPrimitive.Portal>      <ComboboxPrimitive.Positioner        side={side}        sideOffset={sideOffset}        align={align}        alignOffset={alignOffset}        anchor={anchor}        className="isolate z-50"      >        <ComboboxPrimitive.Popup          data-slot="combobox-content"          data-chips={!!anchor}          className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-xl bg-popover text-popover-foreground shadow-lg shadow-black/[0.06] ring-1 ring-overlay-border duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-10 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}          {...props}        />      </ComboboxPrimitive.Positioner>    </ComboboxPrimitive.Portal>  )}function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {  return (    <ComboboxPrimitive.List      data-slot="combobox-list"      className={cn(        "no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",        className      )}      {...props}    />  )}function ComboboxItem({  className,  children,  ...props}: ComboboxPrimitive.Item.Props) {  return (    <ComboboxPrimitive.Item      data-slot="combobox-item"      className={cn(        "relative flex w-full cursor-default items-center gap-2 rounded-lg py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",        className      )}      {...props}    >      {children}      <ComboboxPrimitive.ItemIndicator        render={          <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />        }      >        <CheckIcon className="pointer-events-none" />      </ComboboxPrimitive.ItemIndicator>    </ComboboxPrimitive.Item>  )}function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {  return (    <ComboboxPrimitive.Group      data-slot="combobox-group"      className={cn(className)}      {...props}    />  )}function ComboboxLabel({  className,  ...props}: ComboboxPrimitive.GroupLabel.Props) {  return (    <ComboboxPrimitive.GroupLabel      data-slot="combobox-label"      className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}      {...props}    />  )}function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {  return (    <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />  )}function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {  return (    <ComboboxPrimitive.Empty      data-slot="combobox-empty"      className={cn(        "hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",        className      )}      {...props}    />  )}function ComboboxSeparator({  className,  ...props}: ComboboxPrimitive.Separator.Props) {  return (    <ComboboxPrimitive.Separator      data-slot="combobox-separator"      className={cn("-mx-1 my-1 h-px bg-border", className)}      {...props}    />  )}function ComboboxChips({  className,  ...props}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &  ComboboxPrimitive.Chips.Props) {  return (    <ComboboxPrimitive.Chips      data-slot="combobox-chips"      className={cn(        "flex min-h-10 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",        className      )}      {...props}    />  )}function ComboboxChip({  className,  children,  showRemove = true,  ...props}: ComboboxPrimitive.Chip.Props & {  showRemove?: boolean}) {  return (    <ComboboxPrimitive.Chip      data-slot="combobox-chip"      className={cn(        "flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",        className      )}      {...props}    >      {children}      {showRemove && (        <ComboboxPrimitive.ChipRemove          render={<Button variant="ghost" size="icon-xs" />}          className="-ml-1 opacity-50 hover:opacity-100"          data-slot="combobox-chip-remove"        >          <XIcon className="pointer-events-none" />        </ComboboxPrimitive.ChipRemove>      )}    </ComboboxPrimitive.Chip>  )}function ComboboxChipsInput({  className,  ...props}: ComboboxPrimitive.Input.Props) {  return (    <ComboboxPrimitive.Input      data-slot="combobox-chip-input"      className={cn("min-w-16 flex-1 outline-none", className)}      {...props}    />  )}function useComboboxAnchor() {  return React.useRef<HTMLDivElement | null>(null)}export {  Combobox,  ComboboxInput,  ComboboxContent,  ComboboxList,  ComboboxItem,  ComboboxGroup,  ComboboxLabel,  ComboboxCollection,  ComboboxEmpty,  ComboboxSeparator,  ComboboxChips,  ComboboxChip,  ComboboxChipsInput,  ComboboxTrigger,  ComboboxValue,  useComboboxAnchor,}
    
  3. Make sure the foundation is in place

    Components read their colors, radius and surfaces from the tokens in app/globals.css. See Theming.

Usage#

import {  Combobox,  ComboboxInput,  ComboboxContent,  ComboboxList,  ComboboxItem,  ComboboxGroup,  ComboboxLabel,  ComboboxCollection,  ComboboxEmpty,  ComboboxSeparator,  ComboboxChips,  ComboboxChip,  ComboboxChipsInput,  ComboboxTrigger,  ComboboxValue,} from "@/components/ui/combobox"

Examples#

Auto Highlight

Basic

Clear

Custom

Disabled

Groups

Input Group

Invalid

Multiple

Popup