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
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..."/>
const [value, setValue] = useState('');const [searchText, setSearchText] = useState('');
<Combobox options={options} value={value} onValueChange={setValue} searchable onSearchChange={setSearchText} placeholder="Search and select..."/>
const [selectedValues, setSelectedValues] = useState<string[]>([]);
<Combobox options={options} value={selectedValues} onValueChange={setSelectedValues} multiple placeholder="Select multiple items..."/>
<Combobox label="Preferred Language" options={languageOptions} value={language} onValueChange={setLanguage} required state={isValid ? 'default' : 'error'} errorText="Please select a language" helperText="Choose your primary programming language"/>
Prop | Type | Default | Description |
---|---|---|---|
label | string | - | Optional label text above the combobox |
placeholder | string | 'Select an option...' | Placeholder text when no option is selected |
variant | ComponentVariant | 'default' | Visual style variant |
size | ComponentSize | 'md' | Component size (xs, sm, md, lg, xl) |
state | ComponentState | 'default' | Visual state (default, error, success) |
helperText | string | - | Helper text below the combobox |
errorText | string | - | Error text (shown when state is ‘error’) |
required | boolean | false | Show required indicator (*) |
disabled | boolean | false | Disable the combobox |
searchable | boolean | false | Enable search functionality |
clearable | boolean | false | Show clear button when value is selected |
multiple | boolean | false | Enable multi-selection |
options | ComboboxOption[] | [] | Array of selectable options |
value | string | string[] | - | Selected value(s) |
onValueChange | (value: string | string[]) => void | - | Callback when selection changes |
onSearchChange | (search: string) => void | - | Callback when search text changes |
borderRadius | keyof Theme['components']['borderRadius'] | 'md' | Border radius value |
animationEnabled | boolean | true | Enable animations |
maxDropdownHeight | number | 250 | Maximum height of dropdown |
closeOnSelect | boolean | true | Close dropdown after selection (single-select) |
backgroundColor | string | - | Custom background color |
elevation | number | 3 | Android elevation |
shadowOpacity | number | 0.1 | iOS shadow opacity |
interface ComboboxOption { label: string; // Display text value: string; // Unique value identifier disabled?: boolean; // Option disabled state}
Prop | Type | Description |
---|---|---|
style | StyleProp<ViewStyle> | Container styles |
inputStyle | TextStyle | Trigger input styles |
labelStyle | TextStyle | Label text styles |
helperTextStyle | TextStyle | Helper text styles |
dropdownStyle | StyleProp<ViewStyle> | Dropdown container styles |
optionStyle | StyleProp<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
React.memo
for option lists that don’t change frequentlyUX Guidelines
Search Optimization