logoChisa UI

Command Palette

Search for a command to run...

Componentschevron_rightAnimatedTabs

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 (
<AnimatedTabs
tabs={['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';
4
5export 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);
9
10 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]);
17
18 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 };
30
31 const indicatorAnimatedStyle = useAnimatedStyle(() => ({
32 transform: [{ translateX: translateX.value }],
33 width: indicatorWidth.value,
34 }));
35
36 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 <TouchableOpacity
43 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};
58
59const 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

NameTypeDefaultDescription
tabsstring[]-Array of tab labels
activeIndexnumber0Current active index
onChange(index: number) => void-Change callback