A smooth, animated multiswitch component for React Native with dynamic width support. Perfect for creating segmented controls and tab interfaces with fluid animations.
segmentedControl.mov
tabs.mov
In my last project, there were specific design requirements for segmented control buttons and tabs with customized UI. It was a simple task, but I encountered several problems with the existing solutions I found online:
- Hard to customize styles for my UI requirements
- Same width for all options (no dynamic sizing)
- Based on the old Animated API or no animations at all
- Issues when changing text color for active options - brief moments when text color matches the background
- Issues with setting new values externally, such as based on route parameters
- Issues with language changes - width recalculation was needed
- Instant heavy calculations and screen re-renders (usually these segmented controls were at the top of the screen) mixed with animations - all together when users interact caused FPS drops
- No scrolling if there are multiple options
Solution:
- Width calculated based on each item's layout
- Scrolling enabled by FlatList
onChangeOption
fired after animation is complete, not simultaneouslyonPressItem
for instant reaction if needed- Ref API to get value or force update
- Width recalculation when label is changed
- ๐ฏ Smooth Animations: Powered by react-native-reanimated for 60fps animations
- ๐ฑ Two Variants: Segmented control and tabs styles
- ๐จ Highly Customizable: Extensive styling options for every element
- ๐ Dynamic Width: Automatically adjusts to content width, when language changes
- ๐ Scrollable: Horizontal scrolling for many options
- ๐ช Flexible Alignment: Left, center, or right alignment options
- ๐๏ธ Imperative API: Ref-based methods for programmatic control
npm install react-native-multiswitch-controller
# or
yarn add react-native-multiswitch-controller
This library requires:
react-native-reanimated
>= 3.0.0
import { MultiswitchController } from 'react-native-multiswitch-controller';
function MyComponent() {
const [selectedOption, setSelectedOption] = useState('morning');
return (
<MultiswitchController
options={[
{ value: 'morning', label: '๐
Morning' },
{ value: 'afternoon', label: 'โ๏ธ Afternoon' },
{ value: 'evening', label: '๐ Evening' },
{ value: 'night', label: '๐ Night' },
]}
defaultOption={selectedOption}
onChangeOption={setSelectedOption}
/>
);
}
Prop | Type | Default | Description |
---|---|---|---|
options |
ControlOption<TValue>[] |
required | Array of options to display |
defaultOption |
TValue |
required | Initial selected option |
variant |
'segmentedControl' | 'tabs' |
'segmentedControl' |
Visual style variant |
onChangeOption |
(value: TValue) => void |
- | Callback after animation completes |
onPressItem |
(value: TValue) => void |
- | Instant callback on press |
| ref
| Ref<ControlListRef<TValue>>
| - | Ref for imperative API |
Prop | Type | Default | Description |
---|---|---|---|
containerStyle |
ViewStyle |
- | Main container styles |
inactiveOptionContainerStyle |
ViewStyle |
- | Inactive option container styles |
activeOptionContainerStyle |
ViewStyle |
- | Active option container styles |
inactiveTextStyle |
TextStyle |
- | Inactive text styles |
activeTextStyle |
TextStyle |
- | Active text styles |
containerHeight |
number |
50 |
Height of the main container |
containerPadding |
number |
auto |
Padding around the container |
optionGap |
number |
0 |
Gap between options |
optionHeight |
number |
48 |
Height of individual options |
optionPadding |
number |
0 |
Padding inside options |
align |
'left' | 'center' | 'right' |
'center' |
Alignment of options |
type ControlOption<TValue> = {
value: TValue;
label: string;
};
type ControllerVariant = 'segmentedControl' | 'tabs';
type ControlListRef<TValue> = {
setForcedOption: (value: TValue | null) => void;
activeOption: TValue;
};
<MultiswitchController<TimeOfDay>
variant="segmentedControl"
defaultOption="morning"
options={[
{ value: 'morning', label: '๐
' },
{ value: 'afternoon', label: 'โ๏ธ' },
{ value: 'evening', label: '๐' },
{ value: 'night', label: '๐' },
]}
onChangeOption={onChangeOption}
/>
// Right alignment
<MultiswitchController<'First' | 'Second' | 'Third'>
options={[
{ value: 'First', label: 'First' },
{ value: 'Second', label: 'Second' },
{ value: 'Third', label: 'Third' },
]}
defaultOption="First"
align="right"
/>
For programmatic control without managing state, you can use the imperative ref API:
import { useRef } from 'react';
import {
MultiswitchController,
type ControlListRef,
} from 'react-native-multiswitch-controller';
function MyComponent() {
const controllerRef = useRef<ControlListRef<string>>(null);
const setOption = (option: string) => {
controllerRef.current?.setForcedOption(option);
};
return (
<>
<MultiswitchController
ref={controllerRef}
options={[
{ value: 'morning', label: 'Morning' },
{ value: 'afternoon', label: 'Afternoon' },
{ value: 'evening', label: 'Evening' },
]}
defaultOption="morning"
onChangeOption={(value) => console.log('Selected:', value)}
/>
<Button title="Set Evening" onPress={() => setOption('evening')} />
</>
);
}
Method | Type | Description |
---|---|---|
setForcedOption |
(value: TValue | null) => void |
Programmatically set an option with animation |
activeOption |
TValue |
Read the currently active option |
Note: The imperative API is useful for external control scenarios like changing active option based on route prop
<MultiswitchController<
| 'First'
...
| 'Sixteenth'
>
options={[
{ value: 'First', label: 'First' },
...
{ value: 'Sixteenth', label: 'Sixteenth' },
]}
defaultOption="First"
/>
<MultiswitchController<'First' | 'Second'>
options={[
{ value: 'First', label: 'First is a very long label' },
{ value: 'Second', label: 'Second is short' },
]}
defaultOption="First"
/>
const mockLanguages = {
en: {
food: 'Butterfly',
drink: 'Cheese',
dessert: 'Lettuce',
},
de: {
food: 'Schmetterling',
drink: 'Kรคse',
dessert: 'Salat',
},
};
const [language, setLanguage] = useState<'en' | 'de'>('en');
<MultiswitchController<'food' | 'drink' | 'dessert'>
options={[
{ value: 'food', label: mockLanguages[language].food },
{ value: 'drink', label: mockLanguages[language].drink },
{ value: 'dessert', label: mockLanguages[language].dessert },
]}
defaultOption="food"
/>;
onChangeOption
: Called after the animation completesonPressItem
: Called immediately when an option is pressed
<MultiswitchController
options={options}
defaultOption="option1"
onChangeOption={(value) => {
// Called after animation finishes
console.log('Animation complete, selected:', value);
}}
onPressItem={(value) => {
// Called immediately on press
console.log('Pressed:', value);
}}
/>
- Allow passing SVG instead of text only
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
MIT ยฉ LukasMod