├── .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 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------