Back to Components
📱
Animated Tab Bar
EasyA beautifully animated tab bar component with a sliding indicator, scale effects on active tabs, and smooth transitions. Perfect for segmented controls or navigation tabs.
Navigationv1.0.0Updated 2026-02-04
Live Preview
Open in SnackInstallation
1
Copy AnimatedTabBar.tsx to your components folder
2
Copy haptics.ts utility
3
Import and use
Source Code
AnimatedTabBar.tsx
import React, { useRef, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Animated,
Dimensions,
Platform,
ViewStyle
} from 'react-native';
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
const wp = (percentage: number) => (percentage * SCREEN_WIDTH) / 100;
const hp = (percentage: number) => (percentage * SCREEN_HEIGHT) / 100;
const DEFAULT_COLORS = {
WhiteSmoke: '#F5F5F5',
darkgray: '#374151',
darkgray1: '#4B5563',
BrightPink: '#FF0080',
white: '#FFFFFF',
};
// Mock Haptics
// Haptics Helper
// For native haptic feedback, use the 'native-haptics' component from the registry.
// Example: import { triggerSelectionHaptic } from '../native-haptics/NativeHaptics';
const triggerSelectionHaptic = () => {
// Uncomment detailed implementation in 'native-haptics' component
// triggerSelectionHaptic();
};
interface AnimatedTabBarProps {
tabs: string[];
activeTab: string;
onTabChange: (tab: string) => void;
backgroundColor?: string;
activeTabColor?: string;
tabTextColor?: string;
indicatorColor?: string;
style?: ViewStyle;
}
const AnimatedTabBar: React.FC<AnimatedTabBarProps> = ({
tabs = [],
activeTab,
onTabChange,
backgroundColor,
activeTabColor,
tabTextColor,
indicatorColor,
style
}) => {
// Default theme
const COLORS = DEFAULT_COLORS;
// Safe indexing
const initialIndex = tabs.indexOf(activeTab);
const indicatorAnim = useRef(new Animated.Value(initialIndex !== -1 ? initialIndex : 0)).current;
const TAB_BAR_PADDING = wp(2);
// Calculate tab width dynamically based on number of tabs
const TAB_WIDTH = (wp(90) - TAB_BAR_PADDING * 2) / (tabs.length || 1);
useEffect(() => {
const tabIndex = tabs.indexOf(activeTab);
if (tabIndex !== -1) {
Animated.timing(indicatorAnim, {
toValue: tabIndex,
duration: 300,
useNativeDriver: true,
}).start();
}
}, [activeTab, tabs]);
const getTabScale = (index: number) => ({
transform: [
{
scale: indicatorAnim.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [1, 1.15, 1],
extrapolate: 'clamp',
}),
},
],
});
const getOpacity = (index: number) =>
indicatorAnim.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [0.4, 1, 0.4],
extrapolate: 'clamp',
});
const handleTabPress = (tab: string) => {
triggerSelectionHaptic();
onTabChange(tab);
};
const styles = getStyles(
COLORS,
backgroundColor,
activeTabColor,
tabTextColor,
indicatorColor
);
return (
<View style={[styles.tabBarContainer, style, { paddingHorizontal: TAB_BAR_PADDING }]}>
{/* Moving Indicator */}
<Animated.View
style={[
styles.tabIndicator,
{
width: TAB_WIDTH,
left: TAB_BAR_PADDING,
transform: [
{
translateX: indicatorAnim.interpolate({
inputRange: tabs.map((_, i) => i),
outputRange: tabs.map((_, i) => TAB_WIDTH * i),
}),
},
],
},
]}
/>
{tabs.map((tab, index) => (
<TouchableOpacity
key={tab}
style={styles.tabButton}
activeOpacity={0.8}
onPress={() => handleTabPress(tab)}
>
<Animated.Text
style={[
styles.tabText,
// @ts-ignore
getTabScale(index).transform && { transform: getTabScale(index).transform },
{ opacity: getOpacity(index) },
activeTab === tab && styles.activeTabText,
]}
>
{tab}
</Animated.Text>
</TouchableOpacity>
))}
</View>
);
};
const getStyles = (
COLORS: typeof DEFAULT_COLORS,
backgroundColor?: string,
activeTabColor?: string,
tabTextColor?: string,
indicatorColor?: string
) =>
StyleSheet.create({
tabBarContainer: {
flexDirection: 'row',
backgroundColor: backgroundColor || COLORS.WhiteSmoke,
borderRadius: wp(6),
marginHorizontal: wp(4),
height: hp(5.4),
alignItems: 'center',
position: 'relative',
overflow: 'hidden',
},
tabButton: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
height: '100%',
zIndex: 2,
},
tabText: {
fontSize: hp(1.8),
color: tabTextColor || COLORS.darkgray,
fontWeight: '500',
},
activeTabText: {
color: activeTabColor || COLORS.BrightPink,
fontWeight: '700',
},
tabIndicator: {
position: 'absolute',
left: 0,
height: hp(4.5),
backgroundColor: indicatorColor || COLORS.white,
borderRadius: wp(6),
zIndex: 1,
...Platform.select({
ios: {
shadowColor: indicatorColor || COLORS.darkgray1,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 8,
},
android: {
shadowColor: indicatorColor || COLORS.darkgray1,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.12,
shadowRadius: 8,
elevation: 14,
}
}),
},
});
export default AnimatedTabBar;
Usage Examples
Basic Usage
Simple segmented control
Example 1
const [activeTab, setActiveTab] = useState('All');
<AnimatedTabBar
tabs={['All', 'Active', 'Completed']}
activeTab={activeTab}
onTabChange={setActiveTab}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs* | string[] | - | Array of tab labels |
activeTab* | string | - | Currently active tab |
onTabChange* | (tab: string) => void | - | Callback when tab changes |
hapticEnabled | boolean | true | Enable haptic feedback |
Features
- Smooth sliding indicator animation
- Scale and opacity effects on tabs
- Customizable colors and styling
- Haptic feedback on tab change
- Dynamic number of tabs
- TypeScript ready
📳 Native Capabilities
Includes a zero-dependency native haptic feedback implementation.
View Native ImplementationDependencies
Required:
None