ExpoStartup

Select

A customizable select/dropdown component for choosing from multiple options.

Select

The Select component provides a user-friendly way to select an option from a list of choices. It features a floating label, custom styling, and integrates with React Native's ActionSheet for a native feel.

Features

  • Floating Label: Label animates from placeholder to top position when focused or selected
  • Error Handling: Displays error messages with distinct styling
  • Native ActionSheet: Uses native selection UI for a platform-native experience
  • Customizable Styling: Control for input and container appearance
  • Dark Mode Support: Automatic light/dark mode styles

Import

import Select from '@/components/forms/Select';

Props

PropTypeDefaultDescription
labelstringRequiredText label for the select field
placeholderstring''Placeholder text when no option is selected
optionsArray<{ label: string, value: string | number }>RequiredArray of options to select from
valuestring | numberCurrently selected value
onChange(value: string | number) => voidRequiredFunction called when selection changes
errorstringError message to display below the select
classNamestring''Additional Tailwind classes for the container

Usage Examples

Basic Select

const [selectedCountry, setSelectedCountry] = useState('');
 
const countries = [
  { label: 'United States', value: 'us' },
  { label: 'Canada', value: 'ca' },
  { label: 'United Kingdom', value: 'uk' },
  { label: 'Australia', value: 'au' }
];
 
<Select
  label="Country"
  placeholder="Select a country"
  options={countries}
  value={selectedCountry}
  onChange={setSelectedCountry}
/>

Select with Error

const [category, setCategory] = useState('');
const [categoryError, setCategoryError] = useState('');
 
const categories = [
  { label: 'Electronics', value: 'electronics' },
  { label: 'Clothing', value: 'clothing' },
  { label: 'Books', value: 'books' }
];
 
const validateCategory = (value) => {
  setCategory(value);
  if (!value) {
    setCategoryError('Please select a category');
  } else {
    setCategoryError('');
  }
};
 
<Select
  label="Product Category"
  placeholder="Choose a category"
  options={categories}
  value={category}
  onChange={validateCategory}
  error={categoryError}
/>

Custom Styled Select

<Select
  label="Priority"
  placeholder="Select priority level"
  options={[
    { label: 'Low', value: 'low' },
    { label: 'Medium', value: 'medium' },
    { label: 'High', value: 'high' }
  ]}
  value={priority}
  onChange={setPriority}
  className="mb-8"
/>

Form Integration Example

function ProductForm() {
  const [formData, setFormData] = useState({
    name: '',
    category: '',
    price: '',
    inStock: true
  });
  const [errors, setErrors] = useState({});
 
  const categories = [
    { label: 'Electronics', value: 'electronics' },
    { label: 'Clothing', value: 'clothing' },
    { label: 'Books', value: 'books' },
    { label: 'Home & Kitchen', value: 'home' }
  ];
 
  const updateFormField = (field, value) => {
    setFormData({
      ...formData,
      [field]: value
    });
    
    // Clear error when field is updated
    if (errors[field]) {
      setErrors({
        ...errors,
        [field]: ''
      });
    }
  };
 
  const validateForm = () => {
    let newErrors = {};
    
    if (!formData.name) {
      newErrors.name = 'Product name is required';
    }
    
    if (!formData.category) {
      newErrors.category = 'Category is required';
    }
    
    if (!formData.price) {
      newErrors.price = 'Price is required';
    } else if (isNaN(formData.price)) {
      newErrors.price = 'Price must be a number';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
 
  const handleSubmit = () => {
    if (validateForm()) {
      console.log('Form submitted:', formData);
      // Submit the form data to your API
    }
  };
 
  return (
    <View className="p-4">
      <Input
        label="Product Name"
        value={formData.name}
        onChangeText={(text) => updateFormField('name', text)}
        error={errors.name}
      />
      
      <Select
        label="Category"
        placeholder="Select a category"
        options={categories}
        value={formData.category}
        onChange={(value) => updateFormField('category', value)}
        error={errors.category}
      />
      
      <Input
        label="Price"
        value={formData.price}
        onChangeText={(text) => updateFormField('price', text)}
        keyboardType="numeric"
        error={errors.price}
      />
      
      <View className="flex-row justify-between items-center my-4">
        <Text>In Stock</Text>
        <Switch
          value={formData.inStock}
          onValueChange={(value) => updateFormField('inStock', value)}
        />
      </View>
      
      <Button
        title="Save Product"
        onPress={handleSubmit}
        className="mt-4"
      />
    </View>
  );
}

Filterable Options Example

function FilterableSelect() {
  const [searchQuery, setSearchQuery] = useState('');
  const [selectedItem, setSelectedItem] = useState('');
  const [showOptions, setShowOptions] = useState(false);
  
  const allItems = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Cherry', value: 'cherry' },
    { label: 'Dragonfruit', value: 'dragonfruit' },
    { label: 'Elderberry', value: 'elderberry' },
    { label: 'Fig', value: 'fig' },
    { label: 'Grape', value: 'grape' }
  ];
  
  const filteredItems = searchQuery
    ? allItems.filter(item => 
        item.label.toLowerCase().includes(searchQuery.toLowerCase())
      )
    : allItems;
    
  const selectedOption = allItems.find(item => item.value === selectedItem);
  
  return (
    <View className="mb-6">
      <TouchableOpacity
        onPress={() => setShowOptions(true)}
        className="border border-gray-300 rounded-lg p-3"
      >
        <Text>{selectedOption ? selectedOption.label : 'Select a fruit'}</Text>
      </TouchableOpacity>
      
      <Modal
        visible={showOptions}
        animationType="slide"
        transparent={true}
        onRequestClose={() => setShowOptions(false)}
      >
        <View className="flex-1 justify-end bg-black/50">
          <View className="bg-white rounded-t-xl p-4 h-1/2">
            <View className="flex-row justify-between items-center mb-4">
              <Text className="text-lg font-bold">Select a Fruit</Text>
              <TouchableOpacity onPress={() => setShowOptions(false)}>
                <Icon name="X" size={24} />
              </TouchableOpacity>
            </View>
            
            <View className="border border-gray-300 rounded-lg mb-4">
              <Input
                placeholder="Search fruits..."
                value={searchQuery}
                onChangeText={setSearchQuery}
                rightIcon="Search"
              />
            </View>
            
            <FlatList
              data={filteredItems}
              keyExtractor={(item) => item.value}
              renderItem={({ item }) => (
                <TouchableOpacity
                  onPress={() => {
                    setSelectedItem(item.value);
                    setShowOptions(false);
                  }}
                  className={`p-3 border-b border-gray-100 ${
                    selectedItem === item.value ? 'bg-light-secondary' : ''
                  }`}
                >
                  <Text>{item.label}</Text>
                </TouchableOpacity>
              )}
              ListEmptyComponent={
                <Text className="text-center py-6 text-gray-500">
                  No matching items found
                </Text>
              }
            />
          </View>
        </View>
      </Modal>
    </View>
  );
}

Best Practices

Clear Labels and Placeholders

Use descriptive labels and placeholders that clearly indicate what should be selected:

// Good: Clear, descriptive label and placeholder
<Select
  label="Shipping Method"
  placeholder="Select how you want your order shipped"
  options={shippingOptions}
  // ...
/>
 
// Avoid: Vague labels and placeholders
<Select
  label="Shipping"
  placeholder="Select option"
  options={shippingOptions}
  // ...
/>

Default Values

Provide default selections when appropriate:

// Pre-select the most common option
const [country, setCountry] = useState('us'); // Default to United States
 
<Select
  label="Country"
  options={countries}
  value={country}
  onChange={setCountry}
/>

For long lists, consider using separator headings or indentation in the option labels:

const locationOptions = [
  { label: '-- North America --', value: 'na_header', disabled: true },
  { label: 'United States', value: 'us' },
  { label: 'Canada', value: 'ca' },
  { label: 'Mexico', value: 'mx' },
  { label: '-- Europe --', value: 'eu_header', disabled: true },
  { label: 'United Kingdom', value: 'uk' },
  { label: 'France', value: 'fr' },
  { label: 'Germany', value: 'de' }
];

Implementation Details

The Select component uses React Native's TouchableOpacity for the selection trigger and ActionSheet for displaying the options on iOS. On Android, it uses a custom modal with a similar appearance.

The component features a floating label implementation using Animated, which transitions based on focus state and selection state. The label moves from inside the input (as a placeholder) to above the input when an option is selected.

When the user taps on the component, an action sheet or modal appears showing all available options. Upon selection, the chosen option's label is displayed in the field.

The component uses the useThemeColors hook to apply appropriate color schemes for both light and dark modes.

Notes

  • The component automatically adapts to the platform (iOS or Android)
  • The floating label animation occurs when the select is focused or has a selected option
  • The select border color changes based on focus state and error state
  • The component integrates with form validation patterns similar to the Input component
  • For extremely long lists of options, consider implementing a searchable select with filtering capability

On this page