AnimatedTabs
Segmented control with elastic sliding indicator using layout animations.
9:41
signal_cellular_altwifibattery_full
Installation
$
Usage
Example.tsx
import { AnimatedTabs } from './components/AnimatedTabs';export default function App() {const [activeTab, setActiveTab] = useState(0);return (<AnimatedTabstabs={['Account', 'Password', 'Billings']}activeIndex={activeTab}onChange={setActiveTab}/>);}
Full Component Code
AnimatedTabs.tsx
1import React, { useEffect, useState } from 'react';2import { View, Text, TouchableOpacity, StyleSheet, LayoutChangeEvent } from 'react-native';3import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';45export const AnimatedTabs = ({ tabs, activeIndex, onChange, containerStyle, indicatorStyle, tabStyle, textStyle, activeTextStyle }) => {6 const [layout, setLayout] = useState(new Array(tabs.length).fill({ width: 0, x: 0 }));7 const translateX = useSharedValue(0);8 const indicatorWidth = useSharedValue(0);910 useEffect(() => {11 if (layout[activeIndex]?.width > 0) {12 const activeTab = layout[activeIndex];13 translateX.value = withSpring(activeTab.x, { stiffness: 150, damping: 20 });14 indicatorWidth.value = withSpring(activeTab.width, { stiffness: 150, damping: 20 });15 }16 }, [activeIndex, layout]);1718 const onTabLayout = (event, index) => {19 const { x, width } = event.nativeEvent.layout;20 setLayout(prev => {21 const newLayout = [...prev];22 newLayout[index] = { x, width };23 if (index === activeIndex && prev[index]?.width === 0) {24 translateX.value = x;25 indicatorWidth.value = width;26 }27 return newLayout;28 });29 };3031 const indicatorAnimatedStyle = useAnimatedStyle(() => ({32 transform: [{ translateX: translateX.value }],33 width: indicatorWidth.value,34 }));3536 return (37 <View style={[styles.container, containerStyle]}>38 <Animated.View style={[styles.indicator, indicatorStyle, indicatorAnimatedStyle]} />39 {tabs.map((tab, index) => {40 const isActive = activeIndex === index;41 return (42 <TouchableOpacity43 key={index}44 onLayout={(e) => onTabLayout(e, index)}45 style={[styles.tab, tabStyle]}46 onPress={() => onChange(index)}47 activeOpacity={0.7}48 >49 <Text style={[styles.text, textStyle, isActive && styles.activeText, isActive && activeTextStyle]}>50 {tab}51 </Text>52 </TouchableOpacity>53 );54 })}55 </View>56 );57};5859const styles = StyleSheet.create({60 container: { flexDirection: 'row', backgroundColor: '#F3F4F6', borderRadius: 12, padding: 4, position: 'relative' },61 indicator: { position: 'absolute', top: 4, bottom: 4, left: 0, backgroundColor: '#FFFFFF', borderRadius: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2 },62 tab: { flex: 1, paddingVertical: 8, alignItems: 'center', justifyContent: 'center', borderRadius: 8, zIndex: 1 },63 text: { fontSize: 14, fontWeight: '500', color: '#6B7280' },64 activeText: { color: '#111827', fontWeight: '600' },65});
Props
| Name | Type | Default | Description |
|---|---|---|---|
| tabs | string[] | - | Array of tab labels |
| activeIndex | number | 0 | Current active index |
| onChange | (index: number) => void | - | Change callback |
