AniUI

Combobox

Searchable select with multi-select, groups, clear button, and custom rendering.

Installation#

npx @aniui/cli add combobox
Web preview — components render natively on iOS & Android
import { useState } from "react";
import { Combobox } from "@/components/ui/combobox";

const frameworks = [
  { label: "React Native", value: "rn" },
  { label: "Flutter", value: "flutter" },
  { label: "SwiftUI", value: "swiftui" },
  { label: "Jetpack Compose", value: "compose" },
];

export function MyScreen() {
  const [value, setValue] = useState("");

  return (
    <Combobox
      options={frameworks}
      value={value}
      onValueChange={setValue}
      placeholder="Select framework..."
      searchPlaceholder="Search frameworks..."
    />
  );
}

Usage#

app/index.tsx
import { useState } from "react";
import { Combobox } from "@/components/ui/combobox";

const frameworks = [
  { label: "React Native", value: "rn" },
  { label: "Flutter", value: "flutter" },
  { label: "SwiftUI", value: "swiftui" },
  { label: "Jetpack Compose", value: "compose" },
];

export function MyScreen() {
  const [value, setValue] = useState("");

  return (
    <Combobox
      options={frameworks}
      value={value}
      onValueChange={setValue}
      placeholder="Select framework..."
      searchPlaceholder="Search frameworks..."
    />
  );
}

Multiple Selection#

Enable multi-select mode with the multiple prop. Selected items display as removable chips.

Multi-select
import { useState } from "react";
import { Combobox } from "@/components/ui/combobox";

const tags = [
  { label: "React Native", value: "rn" },
  { label: "TypeScript", value: "ts" },
  { label: "NativeWind", value: "nw" },
  { label: "Expo", value: "expo" },
];

export function MultiSelect() {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <Combobox
      multiple
      options={tags}
      selectedValues={selected}
      onSelectedValuesChange={setSelected}
      placeholder="Select tags..."
    />
  );
}

Groups#

Organize options under group headers using the groups prop. Groups render as sticky section headers.

Grouped options
<Combobox
  groups={[
    {
      label: "Frameworks",
      options: [
        { label: "React Native", value: "rn" },
        { label: "Flutter", value: "flutter" },
      ],
    },
    {
      label: "Languages",
      options: [
        { label: "TypeScript", value: "ts" },
        { label: "Dart", value: "dart" },
      ],
    },
  ]}
  options={[]}
  value={value}
  onValueChange={setValue}
/>

Clear Button#

Add a clear button to reset the selection with clearable.

Clearable
<Combobox
  options={frameworks}
  value={value}
  onValueChange={setValue}
  clearable
  placeholder="Select..."
/>

Custom Items#

Use the renderItem prop for custom option rendering.

Custom item rendering
<Combobox
  options={frameworks}
  value={value}
  onValueChange={setValue}
  renderItem={(option, selected) => (
    <View className="flex-row items-center px-5 py-3 gap-3">
      <View className={cn(
        "h-4 w-4 rounded-full border",
        selected ? "bg-primary border-primary" : "border-input"
      )} />
      <Text className="text-foreground">{option.label}</Text>
    </View>
  )}
/>

Invalid#

Show error styling with the invalid prop.

Invalid state
<Combobox
  options={frameworks}
  value={value}
  onValueChange={setValue}
  invalid
  placeholder="Select framework..."
/>

Disabled#

Prevent interaction with the disabled prop.

Disabled
<Combobox
  options={frameworks}
  value={value}
  onValueChange={setValue}
  disabled
  placeholder="Disabled..."
/>

Use mode="popup" for a button-like trigger style.

Popup mode
<Combobox
  options={frameworks}
  value={value}
  onValueChange={setValue}
  mode="popup"
  placeholder="Choose..."
/>

Props#

ComboboxProps#

PropTypeDefault
options
ComboboxOption[]
-
value
string
-
onValueChange
(value: string) => void
-
multiple
boolean
false
selectedValues
string[]
[]
onSelectedValuesChange
(values: string[]) => void
-
groups
ComboboxGroup[]
-
renderItem
(option, selected) => ReactNode
-
invalid
boolean
false
disabled
boolean
false
clearable
boolean
false
autoHighlight
boolean
false
mode
"select" | "popup"
"select"
placeholder
string
"Select..."
searchPlaceholder
string
"Search..."
emptyText
string
"No results found"
className
string
-
triggerClassName
string
-

ComboboxOption#

PropTypeDefault
label
string
-
value
string
-
disabled
boolean
-

ComboboxGroup#

PropTypeDefault
label
string
-
options
ComboboxOption[]
-

Also accepts all View props from React Native.

Accessibility#

  • Searchable select with type-to-filter functionality.
  • accessibilityRole="button" on trigger and options.
  • accessibilityState tracks selected and disabled states.
  • Clear button has accessibilityLabel="Clear selection".
  • Multi-select chips have individual accessibilityLabel for removal.
  • Uses logical properties for RTL support.

Source#

components/ui/combobox.tsx
import React, { useState, useMemo, useCallback } from "react";
import {
  View,
  TextInput,
  Pressable,
  Text,
  FlatList,
  SectionList,
  Modal,
  ScrollView,
} from "react-native";
import { cn } from "@/lib/utils";
import Svg, { Path } from "react-native-svg";

export interface ComboboxOption {
  label: string;
  value: string;
  disabled?: boolean;
}

export interface ComboboxGroup {
  label: string;
  options: ComboboxOption[];
}

export interface ComboboxProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  options: ComboboxOption[];
  value?: string;
  onValueChange?: (value: string) => void;
  placeholder?: string;
  searchPlaceholder?: string;
  emptyText?: string;
  multiple?: boolean;
  selectedValues?: string[];
  onSelectedValuesChange?: (values: string[]) => void;
  groups?: ComboboxGroup[];
  renderItem?: (option: ComboboxOption, selected: boolean) => React.ReactNode;
  invalid?: boolean;
  disabled?: boolean;
  clearable?: boolean;
  autoHighlight?: boolean;
  mode?: "select" | "popup";
  triggerClassName?: string;
}

function Chip({ label, onRemove }: { label: string; onRemove: () => void }) {
  return (
    <View className="flex-row items-center rounded-full bg-secondary ps-2.5 pe-1 py-0.5 me-1.5 mb-1">
      <Text className="text-xs text-secondary-foreground me-1">{label}</Text>
      <Pressable onPress={onRemove} className="rounded-full p-0.5" accessible={true} accessibilityRole="button" accessibilityLabel={`Remove ${label}`}>
        <Svg width={12} height={12} viewBox="0 0 24 24" fill="none" stroke="#71717a" strokeWidth={2.5}>
          <Path d="M18 6 6 18M6 6l12 12" />
        </Svg>
      </Pressable>
    </View>
  );
}

export function Combobox({
  className, options, value, onValueChange,
  placeholder = "Select...", searchPlaceholder = "Search...", emptyText = "No results found",
  multiple = false, selectedValues = [], onSelectedValuesChange,
  groups, renderItem: renderItemProp, invalid = false, disabled = false,
  clearable = false, autoHighlight = false, mode = "select", triggerClassName,
  ...props
}: ComboboxProps) {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");
  const allOptions = useMemo(() => (groups ? groups.flatMap((g) => g.options) : options), [groups, options]);
  const selected = allOptions.find((o) => o.value === value);
  const isSelected = useCallback((val: string) => (multiple ? selectedValues.includes(val) : val === value), [multiple, selectedValues, value]);
  const filterFn = (opts: ComboboxOption[]) => opts.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()));
  const filteredOptions = useMemo(() => filterFn(allOptions), [allOptions, search]);
  const filteredSections = useMemo(() => {
    if (!groups) return [];
    return groups.map((g) => ({ title: g.label, data: filterFn(g.options) })).filter((s) => s.data.length > 0);
  }, [groups, search]);
  const handleSelect = (val: string) => {
    if (multiple) { const next = selectedValues.includes(val) ? selectedValues.filter((v) => v !== val) : [...selectedValues, val]; onSelectedValuesChange?.(next); }
    else { onValueChange?.(val); setOpen(false); }
    setSearch("");
  };
  const handleClear = () => { if (multiple) onSelectedValuesChange?.([]); else onValueChange?.(""); };
  const hasValue = multiple ? selectedValues.length > 0 : !!value;
  const triggerLabel = multiple ? (selectedValues.length > 0 ? `${selectedValues.length} selected` : placeholder) : selected?.label ?? placeholder;
  // ... renderOption + JSX (see full source on GitHub)
}