Combobox
Searchable select with multi-select, groups, clear button, and custom rendering.
Installation#
npx @aniui/cli add comboboxWeb 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..."
/>Popup Mode#
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
optionsComboboxOption[]-valuestring-onValueChange(value: string) => void-multiplebooleanfalseselectedValuesstring[][]onSelectedValuesChange(values: string[]) => void-groupsComboboxGroup[]-renderItem(option, selected) => ReactNode-invalidbooleanfalsedisabledbooleanfalseclearablebooleanfalseautoHighlightbooleanfalsemode"select" | "popup""select"placeholderstring"Select..."searchPlaceholderstring"Search..."emptyTextstring"No results found"classNamestring-triggerClassNamestring-ComboboxOption#
PropTypeDefault
labelstring-valuestring-disabledboolean-ComboboxGroup#
PropTypeDefault
labelstring-optionsComboboxOption[]-Also accepts all View props from React Native.
Accessibility#
- Searchable select with type-to-filter functionality.
accessibilityRole="button"on trigger and options.accessibilityStatetracks selected and disabled states.- Clear button has
accessibilityLabel="Clear selection". - Multi-select chips have individual
accessibilityLabelfor 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)
}