├── .watchmanconfig ├── tab-bar.gif ├── assets ├── icon.png └── splash.png ├── .gitignore ├── index.js ├── babel.config.js ├── src ├── utils │ └── dimensions.js ├── App.js ├── scenes │ ├── index.js │ └── DefaultScreen │ │ ├── index.js │ │ └── styled.js ├── components │ └── TabBar │ │ ├── TabIcon │ │ └── index.js │ │ ├── styled.js │ │ ├── Tab │ │ ├── styled.js │ │ └── index.js │ │ └── index.js ├── themes │ └── index.js └── navigation │ └── index.js ├── .prettierrc ├── README.md ├── app.json ├── .eslintrc └── package.json /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tab-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-lychko/react-native-bubble-tab-bar/HEAD/tab-bar.gif -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-lychko/react-native-bubble-tab-bar/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-lychko/react-native-bubble-tab-bar/HEAD/assets/splash.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | registerRootComponent(App); 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/dimensions.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const screenWidth = Dimensions.get('window').width; 4 | const screenHeight = Dimensions.get('window').height; 5 | 6 | export { screenWidth, screenHeight }; 7 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AppNavigator from './navigation'; 4 | import ThemeProvider from './themes'; 5 | 6 | const App = () => ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "semi": true, 8 | "useTabs": false, 9 | "parser": "babel", 10 | "jsxBracketSameLine": false, 11 | "arrowParens": "avoid" 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bubble animation tab bar component 2 | 3 | This project was bootstrapped with [Expo](https://expo.io/learn). 4 | 5 | Custom animated TabBar component based on [React Navigation](https://reactnavigation.org). 6 | 7 | ![Demo gif](https://github.com/max-lychko/react-native-bubble-tab-bar/raw/master/tab-bar.gif) -------------------------------------------------------------------------------- /src/scenes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import DefaultScreen from './DefaultScreen'; 4 | 5 | export const HomeScreen = props => ; 6 | export const SearchScreen = props => ; 7 | export const ProfileScreen = props => ; 8 | export const SettingsScreen = props => ; 9 | -------------------------------------------------------------------------------- /src/components/TabBar/TabIcon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Icon from '@expo/vector-icons/SimpleLineIcons'; 5 | 6 | const TabIcon = ({ tintColor, iconName }) => ; 7 | 8 | TabIcon.propTypes = { 9 | tintColor: PropTypes.string.isRequired, 10 | iconName: PropTypes.string.isRequired, 11 | }; 12 | 13 | export default TabIcon; 14 | -------------------------------------------------------------------------------- /src/components/TabBar/styled.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | import styled from 'styled-components'; 4 | 5 | const Wrapper = styled.View` 6 | flex-direction: row; 7 | justify-content: space-around; 8 | align-items: center; 9 | padding-horizontal: 5; 10 | padding-top: 5; 11 | padding-bottom: 34; 12 | border-top-width: ${StyleSheet.hairlineWidth}; 13 | border-top-color: ${({ theme }) => theme.colors.border}; 14 | `; 15 | 16 | export { Wrapper }; 17 | -------------------------------------------------------------------------------- /src/scenes/DefaultScreen/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { theme } from '../../themes'; 5 | 6 | import { Container, Gradient, Title } from './styled'; 7 | 8 | const DefaultScreen = ({ name }) => ( 9 | 10 | 16 | This is the {name} Screen 17 | 18 | ); 19 | 20 | DefaultScreen.propTypes = { 21 | name: PropTypes.string.isRequired, 22 | }; 23 | 24 | export default DefaultScreen; 25 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Bubble Tab Bar", 4 | "slug": "react-native-bubble-tab-bar", 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 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "prettier", 5 | "prettier/react", 6 | "plugin:prettier/recommended", 7 | "eslint-config-prettier" 8 | ], 9 | "parser": "babel-eslint", 10 | "rules": { 11 | "import/no-unresolved": "off", 12 | "react/jsx-filename-extension": [ 13 | 1, 14 | { 15 | "extensions": [".js", ".jsx"] 16 | } 17 | ], 18 | "prettier/prettier": [ 19 | "error", 20 | { 21 | "trailingComma": "es5", 22 | "singleQuote": true, 23 | "printWidth": 100 24 | } 25 | ], 26 | "import/prefer-default-export": "off", 27 | "react/forbid-prop-types": "off", 28 | "react/destructuring-assignment": "off" 29 | }, 30 | "plugins": ["prettier"] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/TabBar/Tab/styled.js: -------------------------------------------------------------------------------- 1 | import { Animated } from 'react-native'; 2 | import styled from 'styled-components'; 3 | 4 | const TabTouchable = styled.TouchableOpacity` 5 | flex: 1; 6 | justify-content: center; 7 | align-items: center; 8 | `; 9 | 10 | const TabWrapper = styled(Animated.View)` 11 | flex-direction: row; 12 | justify-content: center; 13 | align-items: center; 14 | padding-horizontal: 10; 15 | padding-vertical: 5; 16 | border-radius: 20; 17 | background-color: ${({ isActive, activeBgColor }) => (isActive ? activeBgColor : 'transparent')}; 18 | `; 19 | 20 | const Label = styled(Animated.Text)` 21 | margin-left: 5; 22 | font-size: 12; 23 | color: ${({ color }) => color}; 24 | `; 25 | 26 | export { TabTouchable, TabWrapper, Label }; 27 | -------------------------------------------------------------------------------- /src/scenes/DefaultScreen/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { LinearGradient } from 'expo'; 4 | 5 | import { screenHeight, screenWidth } from '../../utils/dimensions'; 6 | 7 | const Container = styled.View` 8 | flex: 1; 9 | background-color: ${({ theme }) => theme.colors.white}; 10 | padding-top: 150; 11 | align-items: center; 12 | `; 13 | 14 | const Gradient = styled(LinearGradient)` 15 | position: absolute; 16 | top: ${screenHeight / -2}; 17 | left: ${(screenWidth * 1.8 - screenWidth) / -2}; 18 | width: ${screenWidth * 1.8}; 19 | height: ${screenHeight}; 20 | border-radius: ${screenHeight / 2}; 21 | `; 22 | 23 | const Title = styled.Text` 24 | max-width: 220; 25 | font-size: 32; 26 | color: ${({ theme }) => theme.colors.white}; 27 | text-align: center; 28 | `; 29 | 30 | export { Container, Gradient, Title }; 31 | -------------------------------------------------------------------------------- /src/components/TabBar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Tab from './Tab'; 5 | 6 | import { Wrapper } from './styled'; 7 | 8 | const TabBar = ({ renderIcon, tabColors, inactiveTintColor, navigation, onTabPress }) => ( 9 | 10 | {navigation.state.routes.map((route, index) => ( 11 | 20 | ))} 21 | 22 | ); 23 | 24 | TabBar.propTypes = { 25 | renderIcon: PropTypes.func.isRequired, 26 | tabColors: PropTypes.array.isRequired, 27 | inactiveTintColor: PropTypes.string.isRequired, 28 | navigation: PropTypes.object.isRequired, 29 | onTabPress: PropTypes.func.isRequired, 30 | }; 31 | 32 | export default TabBar; 33 | -------------------------------------------------------------------------------- /src/themes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ThemeProvider } from 'styled-components'; 4 | 5 | export const theme = { 6 | colors: { 7 | white: '#FFFFFF', 8 | black: '#000000', 9 | inactiveColor: '#262626', 10 | border: '#cbe8d7', 11 | }, 12 | linearGradient: { 13 | header: { 14 | from: '#4252D1', 15 | to: '#609BDB', 16 | }, 17 | }, 18 | tabColors: { 19 | home: { 20 | active: 'rgba(244, 195, 0, 1)', 21 | background: 'rgba(244, 195, 0, 0.1)', 22 | }, 23 | search: { 24 | active: 'rgba(64, 234, 135, 1)', 25 | background: 'rgba(64, 234, 135, 0.1)', 26 | }, 27 | profile: { 28 | active: 'rgba(208, 115, 255, 1)', 29 | background: 'rgba(208, 115, 255, 0.1)', 30 | }, 31 | settings: { 32 | active: 'rgba(235, 92, 110, 1)', 33 | background: 'rgba(235, 92, 110, 0.1)', 34 | }, 35 | }, 36 | }; 37 | 38 | export default props => ; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "private": false, 4 | "scripts": { 5 | "start": "expo start", 6 | "android": "expo start --android", 7 | "ios": "expo start --ios", 8 | "eject": "expo eject", 9 | "lint": "./node_modules/eslint/bin/eslint.js --fix src/", 10 | "pretty": "prettier --config .prettierrc --write src/**/*.js" 11 | }, 12 | "dependencies": { 13 | "@expo/vector-icons": "expo/vector-icons", 14 | "expo": "^32.0.0", 15 | "prop-types": "^15.7.2", 16 | "react": "16.5.0", 17 | "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz", 18 | "react-navigation": "^3.3.2", 19 | "styled-components": "^4.1.3" 20 | }, 21 | "devDependencies": { 22 | "babel-eslint": "^10.0.1", 23 | "babel-preset-expo": "^5.0.0", 24 | "eslint": "^5.15.0", 25 | "eslint-config-airbnb": "^17.1.0", 26 | "eslint-config-prettier": "^4.1.0", 27 | "eslint-plugin-import": "^2.16.0", 28 | "eslint-plugin-jsx-a11y": "^6.2.1", 29 | "eslint-plugin-prettier": "^3.0.1", 30 | "eslint-plugin-react": "^7.12.4", 31 | "husky": "^1.3.1", 32 | "lint-staged": "^8.1.5", 33 | "prettier": "^1.16.4" 34 | }, 35 | "lint-staged": { 36 | "*.js": [ 37 | "yarn pretty", 38 | "yarn lint", 39 | "git add" 40 | ] 41 | }, 42 | "husky": { 43 | "hooks": { 44 | "pre-commit": "lint-staged" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/navigation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createAppContainer, createBottomTabNavigator } from 'react-navigation'; 3 | 4 | import { theme } from '../themes'; 5 | 6 | import { HomeScreen, ProfileScreen, SearchScreen, SettingsScreen } from '../scenes'; 7 | import TabBar from '../components/TabBar'; 8 | import TabIcon from '../components/TabBar/TabIcon'; 9 | 10 | const tabBar = { 11 | Home: { 12 | screen: HomeScreen, 13 | navigationOptions: { 14 | tabBarLabel: 'Home', 15 | tabBarIcon: props => , 16 | }, 17 | }, 18 | Search: { 19 | screen: SearchScreen, 20 | navigationOptions: { 21 | tabBarLabel: 'Search', 22 | tabBarIcon: props => , 23 | }, 24 | }, 25 | Profile: { 26 | screen: ProfileScreen, 27 | navigationOptions: { 28 | tabBarLabel: 'Profile', 29 | tabBarIcon: props => , 30 | }, 31 | }, 32 | Settings: { 33 | screen: SettingsScreen, 34 | navigationOptions: { 35 | tabBarLabel: 'Settings', 36 | tabBarIcon: props => , 37 | }, 38 | }, 39 | }; 40 | 41 | const tabBarConfig = { 42 | tabBarComponent: props => ( 43 | 52 | ), 53 | initialRouteName: 'Home', 54 | tabBarOptions: { 55 | inactiveTintColor: theme.colors.inactiveColor, 56 | }, 57 | }; 58 | 59 | export default createAppContainer(createBottomTabNavigator(tabBar, tabBarConfig)); 60 | -------------------------------------------------------------------------------- /src/components/TabBar/Tab/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Animated } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import { screenWidth } from '../../../utils/dimensions'; 6 | 7 | import { TabTouchable, TabWrapper, Label } from './styled'; 8 | 9 | class Tab extends PureComponent { 10 | static propTypes = { 11 | route: PropTypes.object.isRequired, 12 | isActive: PropTypes.bool.isRequired, 13 | onTabPress: PropTypes.func.isRequired, 14 | renderIcon: PropTypes.func.isRequired, 15 | activeColors: PropTypes.object.isRequired, 16 | inactiveColor: PropTypes.string.isRequired, 17 | }; 18 | 19 | constructor(props) { 20 | super(props); 21 | 22 | this.tabWidth = screenWidth / 4; 23 | 24 | const tabWidth = props.isActive ? this.tabWidth : 50; 25 | const labelOpacity = props.isActive ? 1 : 0; 26 | const labelWidth = props.isActive ? 50 : 0; 27 | 28 | this.state = { 29 | tabWidth: new Animated.Value(tabWidth), 30 | labelOpacity: new Animated.Value(labelOpacity), 31 | labelWidth: new Animated.Value(labelWidth), 32 | }; 33 | } 34 | 35 | componentDidUpdate(prevProps) { 36 | if (prevProps.isActive !== this.props.isActive && prevProps.isActive) { 37 | this.animatedHide(); 38 | } else { 39 | this.animatedOpen(); 40 | } 41 | } 42 | 43 | animatedOpen = () => { 44 | const { tabWidth, labelWidth, labelOpacity } = this.state; 45 | 46 | Animated.parallel([ 47 | Animated.timing(tabWidth, { 48 | toValue: this.tabWidth, 49 | duration: 300, 50 | }).start(), 51 | Animated.timing(labelWidth, { 52 | toValue: 50, 53 | duration: 300, 54 | }).start(), 55 | Animated.timing(labelOpacity, { 56 | toValue: 1, 57 | duration: 150, 58 | delay: 150, 59 | }).start(), 60 | ]); 61 | }; 62 | 63 | animatedHide = () => { 64 | const { tabWidth, labelWidth, labelOpacity } = this.state; 65 | 66 | Animated.parallel([ 67 | Animated.timing(tabWidth, { 68 | toValue: 50, 69 | duration: 300, 70 | }).start(), 71 | Animated.timing(labelWidth, { 72 | toValue: 0, 73 | duration: 300, 74 | }).start(), 75 | Animated.timing(labelOpacity, { 76 | toValue: 0, 77 | duration: 100, 78 | }).start(), 79 | ]); 80 | }; 81 | 82 | render() { 83 | const { route, isActive, onTabPress, renderIcon, activeColors, inactiveColor } = this.props; 84 | const { tabWidth, labelWidth, labelOpacity } = this.state; 85 | 86 | const color = isActive ? activeColors.active : inactiveColor; 87 | 88 | return ( 89 | { 91 | onTabPress({ route }); 92 | }} 93 | > 94 | 99 | {renderIcon({ 100 | route, 101 | focused: isActive, 102 | tintColor: color, 103 | })} 104 | 112 | 113 | 114 | ); 115 | } 116 | } 117 | 118 | export default Tab; 119 | --------------------------------------------------------------------------------