diff --git a/README.md b/README.md
index a3cb049..7df5483 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Perfect for apps that want to:
Powered by React Native Reanimated, it provides butter-smooth animations while maintaining 60 FPS. The library seamlessly integrates with React Navigation's ecosystem while adding a layer of motion and interactivity that makes your app feel more dynamic and responsive.
## 📸 How it looks
+
https://github.com/user-attachments/assets/3b37176b-0ba3-43f7-b1e0-513fb514e825
## Features
@@ -21,6 +22,8 @@ https://github.com/user-attachments/assets/3b37176b-0ba3-43f7-b1e0-513fb514e825
- Built-in icon support
- TypeScript support
- Works with React Navigation
+- Advanced animation configurations
+- Custom animation styles per tab
## Installation
@@ -129,12 +132,11 @@ cd ..
```typescript
import { View } from 'react-native';
-
import { createMotionTabs } from 'react-native-motion-tabs';
import { NavigationContainer } from '@react-navigation/native';
function ExampleScreen() {
- return ;
+ return ;
}
const Tabs = createMotionTabs({
@@ -144,12 +146,33 @@ const Tabs = createMotionTabs({
component: ExampleScreen,
icon: 'home',
iconType: 'Ionicons',
+ animationConfig: {
+ stiffness: 100,
+ overshootClamping: false,
+ restDisplacementThreshold: 0.001,
+ restSpeedThreshold: 0.001,
+ },
+ animationStyle: {
+ scale: 1.2,
+ rotate: 360,
+ opacity: 0.8,
+ },
},
{
name: 'Search',
component: ExampleScreen,
icon: 'search',
iconType: 'Ionicons',
+ animationConfig: {
+ stiffness: 100,
+ overshootClamping: false,
+ restDisplacementThreshold: 0.001,
+ restSpeedThreshold: 0.001,
+ },
+ animationStyle: {
+ scale: 1.1,
+ rotate: 180,
+ },
},
{
name: 'Favorites',
@@ -169,6 +192,12 @@ const Tabs = createMotionTabs({
activeText: '#FFFFFF',
inactiveText: '#000000',
backgroundColor: '#FFFFFF',
+ animationConfig: {
+ stiffness: 100,
+ overshootClamping: false,
+ restDisplacementThreshold: 0.001,
+ restSpeedThreshold: 0.001,
+ },
},
});
@@ -220,3 +249,36 @@ MIT © [Filipi Rafael](https://github.com/filipirafael)
---
Made with ❤️ by [@filipiRafael3](https://x.com/filipiRafael3)
+
+## Animation Configuration
+
+The library uses React Native Reanimated's `withSpring` for animations. Here are the available configuration options:
+
+### Animation Config
+
+- `stiffness`: Controls how "springy" the animation is (default: 100)
+- `overshootClamping`: Prevents the animation from overshooting its target (default: false)
+- `restDisplacementThreshold`: The minimum displacement from the target to consider the animation complete (default: 0.001)
+- `restSpeedThreshold`: The minimum speed to consider the animation complete (default: 0.001)
+
+### Animation Style
+
+- `scale`: Scale factor for the icon when active (default: 1.2)
+- `rotate`: Rotation in degrees for the icon when active (default: 0)
+- `opacity`: Opacity value for the icon when active (default: 1)
+
+Example:
+
+```typescript
+animationConfig: {
+ stiffness: 100,
+ overshootClamping: false,
+ restDisplacementThreshold: 0.001,
+ restSpeedThreshold: 0.001,
+},
+animationStyle: {
+ scale: 1.2,
+ rotate: 360,
+ opacity: 0.8,
+}
+```
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index aaed4d2..c8c643b 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1343,27 +1343,6 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-unistyles (2.20.0):
- - DoubleConversion
- - glog
- - hermes-engine
- - RCT-Folly (= 2024.01.01.00)
- - RCTRequired
- - RCTTypeSafety
- - React-Core
- - React-debug
- - React-Fabric
- - React-featureflags
- - React-graphics
- - React-ImageManager
- - React-NativeModulesApple
- - React-RCTFabric
- - React-rendererdebug
- - React-utils
- - ReactCodegen
- - ReactCommon/turbomodule/bridging
- - ReactCommon/turbomodule/core
- - Yoga
- React-nativeconfig (0.76.5)
- React-NativeModulesApple (0.76.5):
- glog
@@ -1833,7 +1812,6 @@ DEPENDENCIES:
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- - react-native-unistyles (from `../node_modules/react-native-unistyles`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -1958,8 +1936,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
- react-native-unistyles:
- :path: "../node_modules/react-native-unistyles"
React-nativeconfig:
:path: "../node_modules/react-native/ReactCommon"
React-NativeModulesApple:
@@ -2067,7 +2043,6 @@ SPEC CHECKSUMS:
React-Mapbuffer: c174e11bdea12dce07df8669d6c0dc97eb0c7706
React-microtasksnativemodule: 8a80099ad7391f4e13a48b12796d96680f120dc6
react-native-safe-area-context: 458f6b948437afcb59198016b26bbd02ff9c3b47
- react-native-unistyles: 0eb1afdd80a5c6a408e60fb58516d44eb7fea30c
React-nativeconfig: f7ab6c152e780b99a8c17448f2d99cf5f69a2311
React-NativeModulesApple: 70600f7edfc2c2a01e39ab13a20fd59f4c60df0b
React-perflogger: ceb97dd4e5ca6ff20eebb5a6f9e00312dcdea872
diff --git a/src/components/BottomTab/BottomTab.tsx b/src/components/BottomTab/BottomTab.tsx
index 755b619..afdfa65 100644
--- a/src/components/BottomTab/BottomTab.tsx
+++ b/src/components/BottomTab/BottomTab.tsx
@@ -12,7 +12,12 @@ import Animated, {
import { isAndroid } from '../../config/platform';
import { BottomTabButton } from '../BottomTabButton/BottomTabButton';
import { stylesheet } from './styles';
-import { defaultTheme, type StyleConfig } from '../../types';
+import {
+ defaultTheme,
+ type StyleConfig,
+ type AnimationConfig,
+ type IconType,
+} from '../../types';
type DimensionsProps = {
height: number;
@@ -26,7 +31,10 @@ export const BottomTab = ({
tabsConfig,
theme,
}: BottomTabBarProps & { theme?: StyleConfig } & {
- tabsConfig: Record;
+ tabsConfig: Record<
+ string,
+ { icon: string; iconType: IconType; animationConfig?: AnimationConfig }
+ >;
}) => {
const [dimensions, setDimensions] = useState({
height: 20,
@@ -90,9 +98,17 @@ export const BottomTab = ({
const isFocused = state.index === index;
const onPress = () => {
- tabPositionX.value = withSpring(buttonWidth * index, {
- duration: 1500,
- });
+ const config = {
+ stiffness: theme?.animationConfig?.stiffness || 100,
+ overshootClamping:
+ theme?.animationConfig?.overshootClamping || false,
+ restDisplacementThreshold:
+ theme?.animationConfig?.restDisplacementThreshold || 0.001,
+ restSpeedThreshold:
+ theme?.animationConfig?.restSpeedThreshold || 0.001,
+ };
+
+ tabPositionX.value = withSpring(buttonWidth * index, config);
const event = navigation.emit({
type: 'tabPress',
@@ -124,6 +140,7 @@ export const BottomTab = ({
}}
theme={theme || defaultTheme}
label={label}
+ animationConfig={tabsConfig[route.name]?.animationConfig}
/>
);
}
diff --git a/src/components/BottomTabButton/BottomTabButton.tsx b/src/components/BottomTabButton/BottomTabButton.tsx
index 049a042..1280e98 100644
--- a/src/components/BottomTabButton/BottomTabButton.tsx
+++ b/src/components/BottomTabButton/BottomTabButton.tsx
@@ -12,7 +12,12 @@ import Animated, {
interpolate,
} from 'react-native-reanimated';
-import type { TabRoute, StyleConfig } from '../../types';
+import type {
+ TabRoute,
+ StyleConfig,
+ AnimationConfig,
+ AnimationStyleConfig,
+} from '../../types';
import { stylesheet } from './styles';
type Props = {
@@ -22,6 +27,8 @@ type Props = {
route: TabRoute;
theme: StyleConfig;
label: string;
+ animationConfig?: AnimationConfig;
+ animationStyle?: AnimationStyleConfig;
};
export const BottomTabButton = ({
@@ -31,6 +38,8 @@ export const BottomTabButton = ({
route,
theme,
label,
+ animationConfig,
+ animationStyle,
}: Props) => {
const scale = useSharedValue(0);
@@ -69,20 +78,38 @@ export const BottomTabButton = ({
});
const animatedIconStyle = useAnimatedStyle(() => {
- const scaleValue = interpolate(scale.value, [0, 1], [1, 1.2]);
+ const scaleValue = interpolate(
+ scale.value,
+ [0, 1],
+ [1, animationStyle?.scale || 1.2]
+ );
const top = interpolate(scale.value, [0, 1], [0, 9]);
+ const rotate = interpolate(
+ scale.value,
+ [0, 1],
+ [0, animationStyle?.rotate || 0]
+ );
return {
- transform: [{ scale: scaleValue }],
+ transform: [{ scale: scaleValue }, { rotate: `${rotate}deg` }],
top,
+ opacity: animationStyle?.opacity || 1,
};
});
useEffect(() => {
+ const config = {
+ stiffness: animationConfig?.stiffness || 100,
+ overshootClamping: animationConfig?.overshootClamping || false,
+ restDisplacementThreshold:
+ animationConfig?.restDisplacementThreshold || 0.001,
+ restSpeedThreshold: animationConfig?.restSpeedThreshold || 0.001,
+ };
+
scale.value = withSpring(
typeof isFocused === 'boolean' ? (isFocused ? 1 : 0) : isFocused,
- { duration: 350 }
+ config
);
- }, [scale, isFocused]);
+ }, [scale, isFocused, animationConfig]);
const buttonStyle = StyleSheet.flatten([stylesheet.button]);
diff --git a/src/navigation/createMotionTabs.tsx b/src/navigation/createMotionTabs.tsx
index 7ca6875..1bd900b 100644
--- a/src/navigation/createMotionTabs.tsx
+++ b/src/navigation/createMotionTabs.tsx
@@ -1,39 +1,57 @@
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { BottomTab } from '../components/BottomTab/BottomTab';
-import type { MotionTabsConfig, TabRoute } from '../types';
+import type {
+ MotionTabsConfig,
+ IconType,
+ AnimationConfig,
+ AnimationStyleConfig,
+} from '../types';
const Tab = createBottomTabNavigator();
-export function createMotionTabs({ tabs, style, options }: MotionTabsConfig) {
- return function MotionTabs() {
- const tabsConfig = tabs.reduce(
- (acc, tab) => {
- acc[tab.name] = {
- name: tab.name,
- icon: tab.icon || 'circle',
- iconType: tab.iconType || 'Ionicons',
- };
- return acc;
- },
- {} as Record
- );
+export function MotionTabs({ tabs, style, options }: MotionTabsConfig) {
+ const tabsConfig = tabs.reduce(
+ (acc, tab) => {
+ acc[tab.name] = {
+ icon: tab.icon || 'home',
+ iconType: tab.iconType || 'Ionicons',
+ animationConfig: tab.animationConfig,
+ animationStyle: tab.animationStyle,
+ };
+ return acc;
+ },
+ {} as Record<
+ string,
+ {
+ icon: string;
+ iconType: IconType;
+ animationConfig?: AnimationConfig;
+ animationStyle?: AnimationStyleConfig;
+ }
+ >
+ );
- return (
- (
-
- )}
- >
- {tabs.map(({ name, component }) => (
-
- ))}
-
- );
- };
+ const renderTabBar = (props: BottomTabBarProps) => (
+
+ );
+
+ return (
+
+ {tabs.map(({ name, component }) => (
+
+ ))}
+
+ );
+}
+
+export function createMotionTabs(config: MotionTabsConfig) {
+ return () => ;
}
diff --git a/src/types/index.ts b/src/types/index.ts
index bd4992f..efc5e27 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,12 +1,42 @@
-import type { ComponentType } from 'react';
+import type { ComponentType, ReactNode } from 'react';
import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs';
+export type IconType =
+ | 'Ionicons'
+ | 'MaterialIcons'
+ | 'MaterialCommunityIcons'
+ | 'Entypo'
+ | 'FontAwesome'
+ | 'AntDesign'
+ | 'Feather'
+ | 'SimpleLineIcons'
+ | 'Octicons'
+ | 'Zocial';
+
+export type AnimationConfig = {
+ duration?: number;
+ damping?: number;
+ stiffness?: number;
+ mass?: number;
+ overshootClamping?: boolean;
+ restDisplacementThreshold?: number;
+ restSpeedThreshold?: number;
+};
+
+export type AnimationStyleConfig = {
+ scale?: number;
+ rotate?: number;
+ opacity?: number;
+};
+
export type TabConfig = {
name: string;
- component: ComponentType;
+ component: ComponentType<{ children?: ReactNode }>;
icon: string;
- iconType: string;
+ iconType: IconType;
+ animationConfig?: AnimationConfig;
+ animationStyle?: AnimationStyleConfig;
};
export type StyleConfig = {
@@ -17,6 +47,7 @@ export type StyleConfig = {
shadowColor?: string;
tabBarHeight?: number;
marginHorizontal?: number;
+ animationConfig?: AnimationConfig;
};
export const defaultTheme: StyleConfig = {
@@ -27,6 +58,12 @@ export const defaultTheme: StyleConfig = {
shadowColor: '#000000',
tabBarHeight: 60,
marginHorizontal: 40,
+ animationConfig: {
+ stiffness: 100,
+ overshootClamping: false,
+ restDisplacementThreshold: 0.001,
+ restSpeedThreshold: 0.001,
+ },
};
export type MotionTabsConfig = {
@@ -38,7 +75,9 @@ export type MotionTabsConfig = {
export type TabRoute = {
name: string;
icon?: string;
- iconType?: string;
+ iconType?: IconType;
+ animationConfig?: AnimationConfig;
+ animationStyle?: AnimationStyleConfig;
};
export type BottomTabButtonProps = {
@@ -48,4 +87,6 @@ export type BottomTabButtonProps = {
route: TabRoute;
theme: StyleConfig;
label: string;
+ animationConfig?: AnimationConfig;
+ animationStyle?: AnimationStyleConfig;
};