├── .gitignore ├── .watchmanconfig ├── App.js ├── README.md ├── app.json ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── custom-tab-bar.gif ├── package.json ├── src ├── AppEntry.tsx ├── components │ ├── Icon.tsx │ ├── TabBar.tsx │ └── index.tsx ├── router │ └── index.tsx └── screens │ ├── ScreenTemplate.tsx │ └── index.tsx └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import AppEntry from "./src/AppEntry"; 2 | 3 | export default AppEntry; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-custom-tab-bar 2 | Custom animated Tab Bar component that works well with React Navigation 3 | 4 | ![Demo gif](https://github.com/hrastnik/react-native-custom-tab-bar/raw/master/custom-tab-bar.gif) 5 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ExpoPlayground", 4 | "slug": "ExpoPlayground", 5 | "privacy": "public", 6 | "sdkVersion": "32.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android" 10 | ], 11 | "version": "1.0.0", 12 | "orientation": "portrait", 13 | "icon": "./assets/icon.png", 14 | "splash": { 15 | "image": "./assets/splash.png", 16 | "resizeMode": "contain", 17 | "backgroundColor": "#ffffff" 18 | }, 19 | "updates": { 20 | "fallbackToCacheTimeout": 0 21 | }, 22 | "assetBundlePatterns": [ 23 | "**/*" 24 | ], 25 | "ios": { 26 | "supportsTablet": true 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrastnik/react-native-custom-tab-bar/be1d02b8dd799730811c4d6c15a55f44530b1018/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrastnik/react-native-custom-tab-bar/be1d02b8dd799730811c4d6c15a55f44530b1018/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /custom-tab-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrastnik/react-native-custom-tab-bar/be1d02b8dd799730811c4d6c15a55f44530b1018/custom-tab-bar.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "eject": "expo eject" 8 | }, 9 | "dependencies": { 10 | "expo": "^32.0.0", 11 | "react": "16.5.0", 12 | "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz", 13 | "react-native-pose": "^0.9.0", 14 | "react-navigation": "^3.1.2" 15 | }, 16 | "devDependencies": { 17 | "@types/expo": "31.0.9", 18 | "@types/expo__vector-icons": "9.0.0", 19 | "@types/react": "16.8.1", 20 | "@types/react-native": "0.57.32", 21 | "@types/react-navigation": "3.0.1", 22 | "babel-preset-expo": "^5.0.0" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /src/AppEntry.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Router from "./router"; 4 | 5 | export default () => ; 6 | -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from "react"; 2 | import { Text, TextProps } from "react-native"; 3 | 4 | import { Ionicons } from "@expo/vector-icons"; 5 | 6 | const nameMap = { 7 | A: "md-home", 8 | B: "logo-rss", 9 | C: "md-alarm", 10 | D: "md-basket", 11 | E: "md-build" 12 | }; 13 | 14 | const Icon: SFC< 15 | { 16 | name: string; 17 | color: string; 18 | } & TextProps 19 | > = ({ name, color, style, ...props }) => { 20 | return ( 21 | 28 | ); 29 | }; 30 | 31 | export default Icon; 32 | -------------------------------------------------------------------------------- /src/components/TabBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from "react"; 2 | import { View, StyleSheet, TouchableOpacity, Dimensions } from "react-native"; 3 | import posed from "react-native-pose"; 4 | 5 | const windowWidth = Dimensions.get("window").width; 6 | 7 | const Scaler = posed.View({ 8 | active: { scale: 1.25 }, 9 | inactive: { scale: 1 } 10 | }); 11 | 12 | const S = StyleSheet.create({ 13 | container: { 14 | flexDirection: "row", 15 | height: 52, 16 | elevation: 2, 17 | alignItems: "center" 18 | }, 19 | spotlightInner: { width: 48, height: 48, borderRadius: 24 }, 20 | tabButton: { flex: 1 }, 21 | scaler: { flex: 1, alignItems: "center", justifyContent: "center" } 22 | }); 23 | 24 | type Props = { 25 | tabColors: string[]; 26 | renderIcon: SFC<{ 27 | route: any; 28 | focused: boolean; 29 | tintColor: string; 30 | }>; 31 | activeTintColor: string; 32 | inactiveTintColor: string; 33 | onTabPress: ({ route: any }) => void; 34 | onTabLongPress: ({ route: any }) => void; 35 | getAccessibilityLabel: ({ route: any }) => string; 36 | navigation: any; 37 | }; 38 | 39 | class TabBar extends React.Component { 40 | SpotLight = undefined; 41 | spotlightStyle = undefined; 42 | Inner = undefined; 43 | 44 | constructor(props) { 45 | super(props); 46 | this.init(); 47 | } 48 | 49 | componentDidUpdate(prevProps) { 50 | const numTabs = this.props.navigation.state.routes.length; 51 | const prevNumTabs = prevProps.navigation.state.routes.length; 52 | if (numTabs !== prevNumTabs) { 53 | this.init(); 54 | } 55 | } 56 | 57 | init() { 58 | const numTabs = this.props.navigation.state.routes.length; 59 | 60 | const tabWidth = windowWidth / numTabs; 61 | 62 | const poses = Array.from({ length: numTabs }).reduce((poses, _, index) => { 63 | return { ...poses, [`route${index}`]: { x: tabWidth * index } }; 64 | }, {}); 65 | 66 | const styles = StyleSheet.create({ 67 | spotlight: { 68 | width: tabWidth, 69 | height: "100%", 70 | justifyContent: "center", 71 | alignItems: "center" 72 | } 73 | }); 74 | 75 | this.SpotLight = posed.View(poses); 76 | this.spotlightStyle = styles.spotlight; 77 | 78 | const { tabColors } = this.props; 79 | 80 | this.Inner = posed.View({ 81 | passive: { 82 | backgroundColor: [ 83 | "x", 84 | { 85 | inputRange: Array.from({ length: numTabs }).map( 86 | (_, i) => i * tabWidth 87 | ), 88 | outputRange: tabColors 89 | }, 90 | true 91 | ] 92 | } 93 | }); 94 | } 95 | 96 | render() { 97 | const { 98 | renderIcon, 99 | activeTintColor, 100 | inactiveTintColor, 101 | onTabPress, 102 | onTabLongPress, 103 | getAccessibilityLabel, 104 | navigation 105 | } = this.props; 106 | 107 | const { routes, index: activeRouteIndex } = navigation.state; 108 | 109 | const { SpotLight, spotlightStyle, Inner } = this; 110 | 111 | return ( 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | {routes.map((route, routeIndex) => { 120 | const isRouteActive = routeIndex === activeRouteIndex; 121 | const tintColor = isRouteActive ? activeTintColor : inactiveTintColor; 122 | 123 | return ( 124 | { 128 | onTabPress({ route }); 129 | }} 130 | onLongPress={() => { 131 | onTabLongPress({ route }); 132 | }} 133 | accessibilityLabel={getAccessibilityLabel({ route })} 134 | > 135 | 139 | {renderIcon({ route, focused: isRouteActive, tintColor })} 140 | 141 | 142 | ); 143 | })} 144 | 145 | ); 146 | } 147 | } 148 | 149 | export default TabBar; 150 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Icon } from "./Icon"; 2 | export { default as TabBar } from "./TabBar"; 3 | -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { createAppContainer, createBottomTabNavigator } from "react-navigation"; 4 | 5 | import { Icon, TabBar } from "../components"; 6 | import * as Screens from "../screens"; 7 | 8 | const TabNavigator = createBottomTabNavigator( 9 | { 10 | HomeScreen: { 11 | screen: Screens.ScreenA, 12 | navigationOptions: { 13 | tabBarIcon: ({ tintColor }) => 14 | } 15 | }, 16 | SearchScreen: { 17 | screen: Screens.ScreenB, 18 | navigationOptions: { 19 | tabBarIcon: ({ tintColor }) => 20 | } 21 | }, 22 | FavoritesScreen: { 23 | screen: Screens.ScreenC, 24 | navigationOptions: { 25 | tabBarIcon: ({ tintColor }) => 26 | } 27 | }, 28 | ProfileScreen: { 29 | screen: Screens.ScreenD, 30 | navigationOptions: { 31 | tabBarIcon: ({ tintColor }) => 32 | } 33 | }, 34 | ProfileScreen2: { 35 | screen: Screens.ScreenE, 36 | navigationOptions: { 37 | tabBarIcon: ({ tintColor }) => 38 | } 39 | } 40 | }, 41 | { 42 | tabBarComponent: props => ( 43 | 47 | ), 48 | tabBarOptions: { 49 | activeTintColor: "#eeeeee", 50 | inactiveTintColor: "#222222" 51 | } 52 | } 53 | ); 54 | 55 | export default createAppContainer(TabNavigator); 56 | -------------------------------------------------------------------------------- /src/screens/ScreenTemplate.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from "react"; 2 | import { Text, View, StyleSheet } from "react-native"; 3 | 4 | const S = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | backgroundColor: "#bbbbbb", 8 | justifyContent: "center", 9 | alignItems: "center" 10 | }, 11 | text: { fontSize: 28, color: "#222222", textAlign: "center" } 12 | }); 13 | 14 | const ScreenTemplate: SFC<{ name: string; color: string }> = ({ 15 | name, 16 | color 17 | }) => ( 18 | 19 | This is the "{name}" screen 20 | 21 | ); 22 | 23 | export default ScreenTemplate; 24 | -------------------------------------------------------------------------------- /src/screens/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Screen from "./ScreenTemplate"; 4 | 5 | export const ScreenA = () => ; 6 | export const ScreenB = () => ; 7 | export const ScreenC = () => ; 8 | export const ScreenD = () => ; 9 | export const ScreenE = () => ; 10 | --------------------------------------------------------------------------------