Selection Limits
- Set reasonable maximum selection limits for better UX
- Provide clear feedback when selection limits are reached
- Consider using visual indicators for minimum/maximum requirements
SelectableTag provides an elegant solution for creating interactive tag lists with selection states. It features smooth animations, visual feedback, and a flexible container system for organizing multiple tags in various layouts.
import { SelectableTag, SelectableTagContainerList, useItemSelectTag} from 'rnc-theme';
<SelectableTag label="React Native" checked={false} onPress={() => handleSelect()}/>
<SelectableTagContainerList> <SelectableTag label="JavaScript" checked={true} onPress={() => handleSelect()} /> <SelectableTag label="TypeScript" checked={false} onPress={() => handleSelect()} /></SelectableTagContainerList>
const items = [ { id: 1, name: 'Frontend', selected: false }, { id: 2, name: 'Backend', selected: false },];
const { items: tags, toggleItems } = useItemSelectTag(items);
<SelectableTagContainerList> {tags.map((tag) => ( <SelectableTag key={tag.id} label={tag.name} checked={tag.selected} onPress={() => toggleItems(tag.id)} /> ))}</SelectableTagContainerList>
Prop | Type | Default | Description |
---|---|---|---|
label | string | - | Text content to display on the tag |
checked | boolean | - | Selection state of the tag |
onPress | () => void | - | Callback function when tag is pressed |
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | SelectableTag components to render |
...props | ViewProps | - | Additional View props |
Parameter | Type | Description |
---|---|---|
itemInit | ItemSelectTag[] | Initial array of items with selection state |
Property | Type | Description |
---|---|---|
items | ItemSelectTag[] | Current array of items with their states |
toggleItems | (id: number) => void | Function to toggle selection state by ID |
type ItemSelectTag = { id: number; name: string; selected: boolean;};
const skillsData = [ { id: 1, name: 'React', selected: false }, { id: 2, name: 'Vue.js', selected: false }, { id: 3, name: 'Angular', selected: false }, { id: 4, name: 'Node.js', selected: false }, { id: 5, name: 'Python', selected: false }, { id: 6, name: 'Java', selected: false },];
const SkillsSelector = () => { const { items, toggleItems } = useItemSelectTag(skillsData);
return ( <VStack spacing="md"> <Title>Select Your Skills</Title> <SelectableTagContainerList> {items.map((skill) => ( <SelectableTag key={skill.id} label={skill.name} checked={skill.selected} onPress={() => toggleItems(skill.id)} /> ))} </SelectableTagContainerList> </VStack> );};
const categoriesData = [ { id: 1, name: 'Technology', selected: false }, { id: 2, name: 'Design', selected: false }, { id: 3, name: 'Business', selected: false }, { id: 4, name: 'Marketing', selected: false }, { id: 5, name: 'Science', selected: false },];
const CategoryFilter = ({ onFiltersChange }) => { const { items, toggleItems } = useItemSelectTag(categoriesData);
useEffect(() => { const selectedCategories = items .filter(item => item.selected) .map(item => item.name); onFiltersChange(selectedCategories); }, [items, onFiltersChange]);
return ( <VStack spacing="sm"> <Text variant="label">Filter by Category</Text> <SelectableTagContainerList> {items.map((category) => ( <SelectableTag key={category.id} label={category.name} checked={category.selected} onPress={() => toggleItems(category.id)} /> ))} </SelectableTagContainerList> </VStack> );};
const interestsData = [ { id: 1, name: 'K-pop', selected: false }, { id: 2, name: 'K-drama', selected: false }, { id: 3, name: 'Variety Show', selected: false }, { id: 4, name: 'Webtoon', selected: false }, { id: 5, name: 'Fashion', selected: false }, { id: 6, name: 'Beauty', selected: false }, { id: 7, name: 'Dance Cover', selected: false }, { id: 8, name: 'Fanart', selected: false }, { id: 9, name: 'Idol News', selected: false }, { id: 10, name: 'Korean Food', selected: false },];
const InterestsSurvey = () => { const { items, toggleItems } = useItemSelectTag(interestsData); const [submitted, setSubmitted] = useState(false);
const selectedCount = items.filter(item => item.selected).length;
const handleSubmit = () => { const selectedInterests = items .filter(item => item.selected) .map(item => item.name);
console.log('Selected interests:', selectedInterests); setSubmitted(true); };
return ( <VScroll style={{ flex: 1, padding: 16 }}> <VStack spacing="lg"> <VStack spacing="sm"> <Title>What are your interests?</Title> <Text variant="secondary"> Select at least 3 topics you're interested in </Text> </VStack>
<SelectableTagContainerList> {items.map((interest) => ( <SelectableTag key={interest.id} label={interest.name} checked={interest.selected} onPress={() => toggleItems(interest.id)} /> ))} </SelectableTagContainerList>
<VStack spacing="sm"> <Text variant="caption"> {selectedCount} interests selected </Text>
<Button variant="primary" disabled={selectedCount < 3} onPress={handleSubmit} > <ButtonText>Continue</ButtonText> </Button> </VStack> </VStack> </VScroll> );};
const DynamicTagSelector = () => { const [searchQuery, setSearchQuery] = useState(''); const [allTags] = useState([ { id: 1, name: 'JavaScript', selected: false }, { id: 2, name: 'TypeScript', selected: false }, { id: 3, name: 'React', selected: false }, { id: 4, name: 'Vue.js', selected: false }, { id: 5, name: 'Angular', selected: false }, { id: 6, name: 'Node.js', selected: false }, { id: 7, name: 'Python', selected: false }, { id: 8, name: 'Java', selected: false }, ]);
const { items, toggleItems } = useItemSelectTag(allTags);
const filteredItems = items.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()) );
return ( <VStack spacing="md" style={{ flex: 1, padding: 16 }}> <VStack spacing="sm"> <Title>Select Technologies</Title> <TextInput placeholder="Search technologies..." value={searchQuery} onChangeText={setSearchQuery} /> </VStack>
<SelectableTagContainerList> {filteredItems.map((tag) => ( <SelectableTag key={tag.id} label={tag.name} checked={tag.selected} onPress={() => toggleItems(tag.id)} /> ))} </SelectableTagContainerList>
{filteredItems.length === 0 && ( <Text variant="secondary" style={{ textAlign: 'center' }}> No technologies found matching "{searchQuery}" </Text> )} </VStack> );};
const useAdvancedItemSelectTag = ( itemInit: ItemSelectTag[], maxSelection?: number) => { const [items, setItems] = useState(itemInit);
const toggleItems = useCallback((id: number) => { setItems((prevItems) => { const selectedCount = prevItems.filter(item => item.selected).length;
return prevItems.map((item) => { if (item.id === id) { // Check if we can select more items if (!item.selected && maxSelection && selectedCount >= maxSelection) { return item; // Don't select if max reached }
return { ...item, selected: !item.selected, }; } return item; }); }); }, [maxSelection]);
const clearAll = useCallback(() => { setItems(prevItems => prevItems.map(item => ({ ...item, selected: false })) ); }, []);
const selectAll = useCallback(() => { setItems(prevItems => prevItems.map(item => ({ ...item, selected: true })) ); }, []);
const selectedItems = items.filter(item => item.selected);
return { items, toggleItems, clearAll, selectAll, selectedItems, selectedCount: selectedItems.length, };};
const CustomTagContainer = ({ children, columns = 2 }) => { const itemsPerRow = Math.ceil(children.length / columns);
return ( <View style={{ flexDirection: 'column', gap: 12 }}> {Array.from({ length: columns }, (_, columnIndex) => ( <View key={columnIndex} style={{ flexDirection: 'row', gap: 12 }}> {children.slice( columnIndex * itemsPerRow, (columnIndex + 1) * itemsPerRow )} </View> ))} </View> );};
const AnimatedTagCounter = ({ count, maxCount }) => { const animatedValue = useSharedValue(0);
useEffect(() => { animatedValue.value = withSpring(count); }, [count]);
const animatedStyle = useAnimatedStyle(() => { return { transform: [{ scale: animatedValue.value > 0 ? 1.1 : 1 }], }; });
return ( <Animated.View style={[animatedStyle, { alignItems: 'center' }]}> <Text variant="caption"> {count} / {maxCount} selected </Text> </Animated.View> );};
The SelectableTag component uses React Native Reanimated with the following timing configuration:
const TimingConfig = { duration: 150,};
// Custom timing for faster animationsconst FastTimingConfig = { duration: 100,};
// Custom spring animationconst SpringConfig = { damping: 15, stiffness: 150, mass: 1,};
Selection Limits
Performance
React.memo
for tag lists with many itemsLayout