Skip to content

Combobox

Combobox provides a comprehensive dropdown selection solution with built-in search functionality, multi-select support, animated interactions, and flexible positioning. It features smooth animations, keyboard support, and adaptive positioning for optimal user experience.

import { Combobox } from 'rnc-theme';
import type { ComboboxProps, ComboboxOption } from 'rnc-theme';
const [value, setValue] = useState('');
const options = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' },
];
<Combobox
options={options}
value={value}
onValueChange={setValue}
placeholder="Select a fruit..."
/>
PropTypeDefaultDescription
labelstring-Optional label text above the combobox
placeholderstring'Select an option...'Placeholder text when no option is selected
variantComponentVariant'default'Visual style variant
sizeComponentSize'md'Component size (xs, sm, md, lg, xl)
stateComponentState'default'Visual state (default, error, success)
helperTextstring-Helper text below the combobox
errorTextstring-Error text (shown when state is ‘error’)
requiredbooleanfalseShow required indicator (*)
disabledbooleanfalseDisable the combobox
searchablebooleanfalseEnable search functionality
clearablebooleanfalseShow clear button when value is selected
multiplebooleanfalseEnable multi-selection
optionsComboboxOption[][]Array of selectable options
valuestring | string[]-Selected value(s)
onValueChange(value: string | string[]) => void-Callback when selection changes
onSearchChange(search: string) => void-Callback when search text changes
borderRadiuskeyof Theme['components']['borderRadius']'md'Border radius value
animationEnabledbooleantrueEnable animations
maxDropdownHeightnumber250Maximum height of dropdown
closeOnSelectbooleantrueClose dropdown after selection (single-select)
backgroundColorstring-Custom background color
elevationnumber3Android elevation
shadowOpacitynumber0.1iOS shadow opacity
interface ComboboxOption {
label: string; // Display text
value: string; // Unique value identifier
disabled?: boolean; // Option disabled state
}
PropTypeDescription
styleStyleProp<ViewStyle>Container styles
inputStyleTextStyleTrigger input styles
labelStyleTextStyleLabel text styles
helperTextStyleTextStyleHelper text styles
dropdownStyleStyleProp<ViewStyle>Dropdown container styles
optionStyleStyleProp<ViewStyle>Individual option styles
const UserProfileForm = () => {
const [country, setCountry] = useState('');
const [languages, setLanguages] = useState<string[]>([]);
const [role, setRole] = useState('');
const countryOptions = [
{ label: 'United States', value: 'us' },
{ label: 'United Kingdom', value: 'uk' },
{ label: 'Canada', value: 'ca' },
{ label: 'Australia', value: 'au' },
{ label: 'Germany', value: 'de' },
];
const languageOptions = [
{ label: 'English', value: 'en' },
{ label: 'Spanish', value: 'es' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Chinese', value: 'zh' },
];
const roleOptions = [
{ label: 'Frontend Developer', value: 'frontend' },
{ label: 'Backend Developer', value: 'backend' },
{ label: 'Full Stack Developer', value: 'fullstack' },
{ label: 'DevOps Engineer', value: 'devops' },
{ label: 'UI/UX Designer', value: 'designer' },
];
return (
<VStack spacing="lg" padding="xl">
<Combobox
label="Country"
options={countryOptions}
value={country}
onValueChange={setCountry}
searchable
required
placeholder="Select your country..."
helperText="Choose your primary residence"
/>
<Combobox
label="Languages"
options={languageOptions}
value={languages}
onValueChange={setLanguages}
multiple
searchable
clearable
placeholder="Select languages you speak..."
helperText="Select all languages you're fluent in"
/>
<Combobox
label="Role"
options={roleOptions}
value={role}
onValueChange={setRole}
required
placeholder="Select your role..."
state={role ? 'success' : 'default'}
/>
</VStack>
);
};
const DynamicCombobox = () => {
const [selectedCity, setSelectedCity] = useState('');
const [cities, setCities] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const searchCities = async (query: string) => {
if (query.length < 2) {
setCities([]);
return;
}
setLoading(true);
try {
const response = await fetch(`/api/cities?search=${query}`);
const data = await response.json();
setCities(data.map(city => ({
label: `${city.name}, ${city.country}`,
value: city.id
})));
} catch (error) {
console.error('Failed to fetch cities:', error);
} finally {
setLoading(false);
}
};
const handleSearchChange = (text: string) => {
setSearchQuery(text);
searchCities(text);
};
return (
<Combobox
label="City"
options={cities}
value={selectedCity}
onValueChange={setSelectedCity}
searchable
onSearchChange={handleSearchChange}
placeholder="Type to search cities..."
helperText={loading ? 'Searching...' : 'Start typing to find cities'}
/>
);
};
const CategoryMultiSelect = () => {
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
const skillOptions = [
// Frontend
{ label: 'React', value: 'react' },
{ label: 'Vue.js', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'TypeScript', value: 'typescript' },
// Backend
{ label: 'Node.js', value: 'nodejs' },
{ label: 'Python', value: 'python' },
{ label: 'Java', value: 'java' },
{ label: 'Go', value: 'go' },
// Mobile
{ label: 'React Native', value: 'react-native' },
{ label: 'Flutter', value: 'flutter' },
{ label: 'Swift', value: 'swift' },
{ label: 'Kotlin', value: 'kotlin' },
];
const handleSkillsChange = (skills: string | string[]) => {
setSelectedSkills(skills as string[]);
};
return (
<VStack spacing="md">
<Combobox
label="Technical Skills"
options={skillOptions}
value={selectedSkills}
onValueChange={handleSkillsChange}
multiple
searchable
clearable
placeholder="Select your skills..."
helperText={`${selectedSkills.length} skills selected`}
maxDropdownHeight={300}
/>
{selectedSkills.length > 0 && (
<Card>
<Text style={{ fontWeight: 'bold', marginBottom: 8 }}>
Selected Skills:
</Text>
<HStack spacing="xs" wrap>
{selectedSkills.map(skillValue => {
const skill = skillOptions.find(s => s.value === skillValue);
return (
<Badge key={skillValue} variant="primary">
{skill?.label}
</Badge>
);
})}
</HStack>
</Card>
)}
</VStack>
);
};
<VStack spacing="lg" padding="lg">
<Combobox
size="xs"
options={options}
value={value}
onValueChange={setValue}
placeholder="Extra Small"
/>
<Combobox
size="sm"
options={options}
value={value}
onValueChange={setValue}
placeholder="Small"
/>
<Combobox
size="md"
options={options}
value={value}
onValueChange={setValue}
placeholder="Medium (Default)"
/>
<Combobox
size="lg"
options={options}
value={value}
onValueChange={setValue}
placeholder="Large"
/>
<Combobox
size="xl"
options={options}
value={value}
onValueChange={setValue}
placeholder="Extra Large"
/>
</VStack>
const StyledCombobox = () => {
return (
<Combobox
label="Custom Styled"
options={options}
value={value}
onValueChange={setValue}
searchable
clearable
borderRadius="xl"
backgroundColor="#f8f9ff"
style={{
borderWidth: 2,
borderColor: '#e0e7ff',
}}
labelStyle={{
color: '#4338ca',
fontSize: 16,
fontWeight: 'bold',
}}
dropdownStyle={{
borderRadius: 16,
borderWidth: 2,
borderColor: '#e0e7ff',
}}
optionStyle={{
paddingVertical: 16,
}}
elevation={6}
shadowOpacity={0.15}
/>
);
};
const ValidationExample = () => {
const [value, setValue] = useState('');
const [touched, setTouched] = useState(false);
const isValid = value.length > 0;
const showError = touched && !isValid;
return (
<VStack spacing="lg">
<Combobox
label="Required Field"
options={options}
value={value}
onValueChange={(newValue) => {
setValue(newValue as string);
setTouched(true);
}}
required
state={showError ? 'error' : isValid ? 'success' : 'default'}
errorText={showError ? 'Please select an option' : undefined}
helperText={!showError ? 'This field is required' : undefined}
placeholder="Select an option..."
/>
<Combobox
label="Success State"
options={options}
value="success-value"
onValueChange={() => {}}
state="success"
helperText="Great choice!"
disabled
/>
<Combobox
label="Error State"
options={options}
value=""
onValueChange={() => {}}
state="error"
errorText="Invalid selection"
placeholder="Error example..."
/>
</VStack>
);
};
const CustomOptionCombobox = () => {
const userOptions = [
{
label: 'John Doe',
value: 'john',
avatar: 'https://avatar.url/john.jpg',
role: 'Admin'
},
{
label: 'Jane Smith',
value: 'jane',
avatar: 'https://avatar.url/jane.jpg',
role: 'User'
},
];
// Note: This would require extending the component
// to support custom option rendering
return (
<Combobox
label="Assign User"
options={userOptions.map(user => ({
label: `${user.label} (${user.role})`,
value: user.value
}))}
value={selectedUser}
onValueChange={setSelectedUser}
searchable
placeholder="Search users..."
/>
);
};
const AsyncSearchCombobox = () => {
const [options, setOptions] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
const [searchValue, setSearchValue] = useState('');
// Debounced search function
const debouncedSearch = useCallback(
debounce(async (query: string) => {
if (query.length < 2) {
setOptions([]);
return;
}
setLoading(true);
try {
const results = await searchAPI(query);
setOptions(results.map(item => ({
label: item.name,
value: item.id,
disabled: !item.available
})));
} catch (error) {
console.error('Search failed:', error);
setOptions([]);
} finally {
setLoading(false);
}
}, 300),
[]
);
const handleSearchChange = (text: string) => {
setSearchValue(text);
debouncedSearch(text);
};
return (
<Combobox
label="Search Products"
options={options}
value={selectedProduct}
onValueChange={setSelectedProduct}
searchable
onSearchChange={handleSearchChange}
placeholder="Type to search products..."
helperText={loading ? 'Searching...' : `${options.length} results`}
maxDropdownHeight={400}
/>
);
};
const GroupedOptionsCombobox = () => {
// Simulate grouped options by using separators
const groupedOptions = [
// Fruits group
{ label: '--- Fruits ---', value: 'fruits-header', disabled: true },
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' },
// Vegetables group
{ label: '--- Vegetables ---', value: 'vegetables-header', disabled: true },
{ label: 'Carrot', value: 'carrot' },
{ label: 'Broccoli', value: 'broccoli' },
{ label: 'Spinach', value: 'spinach' },
];
return (
<Combobox
label="Food Items"
options={groupedOptions}
value={selectedFood}
onValueChange={setSelectedFood}
searchable
placeholder="Select food item..."
maxDropdownHeight={300}
/>
);
};

The Combobox component includes sophisticated animations that can be customized:

// Animation is enabled by default, but can be disabled
<Combobox
animationEnabled={false} // Disable all animations
options={options}
value={value}
onValueChange={setValue}
/>
// Animations include:
// - Smooth dropdown open/close with spring physics
// - Chevron rotation indicator
// - Scale feedback on trigger press
// - Opacity transitions for modal overlay
// - Adaptive positioning based on available screen space

Performance

  • Use React.memo for option lists that don’t change frequently
  • Implement debouncing for search functionality to reduce API calls
  • Consider virtualizing very long option lists (100+ items)
  • Avoid creating new option arrays on every render

UX Guidelines

  • Use single-select for mutually exclusive choices
  • Use multi-select when users need to pick multiple related items
  • Provide clear feedback for loading and error states
  • Keep option lists focused and relevant to the context

Search Optimization

  • Implement fuzzy search for better user experience
  • Show “no results” state with helpful suggestions
  • Consider highlighting matching text in search results
  • Provide search hints or examples for complex queries