├── .gitignore
├── .prettierrc
├── README.md
├── app.json
├── app
├── (tabs)
│ ├── _layout.tsx
│ ├── explore.tsx
│ ├── index.tsx
│ └── profile.tsx
├── +html.tsx
└── _layout.tsx
├── assets
├── explore.jpg
├── home.jpg
└── profile.jpg
├── babel.config.js
├── components
└── TabBarButton.tsx
├── constants
└── Colors.ts
├── icons
├── ExploreIcon.tsx
├── HomeIcon.tsx
└── ProfileIcon.tsx
├── metro.config.js
├── package.json
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
38 | # The following patterns were generated by expo-cli
39 |
40 | expo-env.d.ts
41 | # @end expo-cli
42 |
43 | ios
44 | android
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Expo Router tabs with Reanimated demo
2 |
3 | https://github.com/kacperkapusciak/reanimated-expo-router-tabs/assets/39658211/12f73b62-f5fd-4830-8cb4-db8010f864c9
4 |
5 | ## Run instructions:
6 |
7 | ### Clone and cd into the project
8 |
9 | ```sh
10 | git clone https://github.com/kacperkapusciak/reanimated-expo-router-tabs.git
11 | cd reanimated-expo-router-tabs
12 | ```
13 |
14 | ### Install dependencies
15 |
16 | ```sh
17 | npm install
18 | ```
19 |
20 | ### Run the project using Expo Go
21 |
22 | ```sh
23 | npx expo start -c
24 | ```
25 |
26 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "reanimated-expo-router-tabs",
4 | "slug": "reanimated-expo-router-tabs",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "scheme": "myapp",
8 | "userInterfaceStyle": "automatic",
9 | "assetBundlePatterns": [
10 | "**/*"
11 | ],
12 | "plugins": [
13 | "expo-router"
14 | ],
15 | "experiments": {
16 | "typedRoutes": true
17 | },
18 | "ios": {
19 | "bundleIdentifier": "com.kacperkapusciak.reanimated-expo-router-tabs"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs, usePathname } from "expo-router";
2 | import { StyleSheet } from "react-native";
3 |
4 | import { BlurView } from "expo-blur";
5 | import { TabBarButton } from "../../components/TabBarButton";
6 | import { HomeIcon } from "../../icons/HomeIcon";
7 | import { ExploreIcon } from "../../icons/ExploreIcon";
8 | import { ProfileIcon } from "../../icons/ProfileIcon";
9 |
10 | export default function TabLayout() {
11 | const pathname = usePathname();
12 | return (
13 | (
19 |
24 | ),
25 | }}
26 | >
27 | (
31 |
37 | ),
38 | }}
39 | />
40 | (
44 |
50 | ),
51 | }}
52 | />
53 | (
57 |
63 | ),
64 | }}
65 | />
66 |
67 | );
68 | }
69 |
70 | const styles = StyleSheet.create({
71 | tabBarLabelStyle: {
72 | fontSize: 11,
73 | },
74 | tabBarStyle: {
75 | position: "absolute",
76 | borderTopWidth: 0,
77 | },
78 | });
79 |
--------------------------------------------------------------------------------
/app/(tabs)/explore.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 | import { Image } from "expo-image";
3 |
4 | const image = require("../../assets/explore.jpg");
5 |
6 | export default function ExploreScreen() {
7 | return ;
8 | }
9 |
10 | const styles = StyleSheet.create({
11 | image: {
12 | width: 400,
13 | height: 860,
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/app/(tabs)/index.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 | import { Image } from "expo-image";
3 |
4 | const image = require("../../assets/home.jpg");
5 |
6 | export default function HomeScreen() {
7 | return ;
8 | }
9 |
10 | const styles = StyleSheet.create({
11 | image: {
12 | width: 400,
13 | height: 860,
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/app/(tabs)/profile.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 | import { Image } from "expo-image";
3 |
4 | const image = require("../../assets/profile.jpg");
5 |
6 | export default function ProfileScreen() {
7 | return ;
8 | }
9 |
10 | const styles = StyleSheet.create({
11 | image: {
12 | width: 400,
13 | height: 860,
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from 'expo-router/html';
2 |
3 | // This file is web-only and used to configure the root HTML for every
4 | // web page during static rendering.
5 | // The contents of this function only run in Node.js environments and
6 | // do not have access to the DOM or browser APIs.
7 | export default function Root({ children }: { children: React.ReactNode }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | {/*
15 | This viewport disables scaling which makes the mobile website act more like a native app.
16 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
17 |
18 | */}
19 |
23 | {/*
24 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
25 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
26 | */}
27 |
28 |
29 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
30 |
31 | {/* Add any additional elements that you want globally available on web... */}
32 |
33 | {children}
34 |
35 | );
36 | }
37 |
38 | const responsiveBackground = `
39 | body {
40 | background-color: #fff;
41 | }
42 | @media (prefers-color-scheme: dark) {
43 | body {
44 | background-color: #000;
45 | }
46 | }`;
47 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from "expo-router";
2 |
3 | export { ErrorBoundary } from "expo-router";
4 |
5 | export const unstable_settings = {
6 | initialRouteName: "(tabs)",
7 | };
8 |
9 | export default function RootLayout() {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/assets/explore.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kacperkapusciak/reanimated-expo-router-tabs/e69cdc870770cf442accbeaa4cded5da7f0406ed/assets/explore.jpg
--------------------------------------------------------------------------------
/assets/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kacperkapusciak/reanimated-expo-router-tabs/e69cdc870770cf442accbeaa4cded5da7f0406ed/assets/home.jpg
--------------------------------------------------------------------------------
/assets/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kacperkapusciak/reanimated-expo-router-tabs/e69cdc870770cf442accbeaa4cded5da7f0406ed/assets/profile.jpg
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [
6 | // Required for expo-router
7 | 'expo-router/babel',
8 | 'react-native-reanimated/plugin'
9 | ],
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/components/TabBarButton.tsx:
--------------------------------------------------------------------------------
1 | import { Pressable, StyleSheet, Text } from "react-native";
2 | import { ACTIVE_COLOR, INACTIVE_COLOR } from "../constants/colors";
3 | import Animated, {
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withSpring,
7 | } from "react-native-reanimated";
8 |
9 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
10 |
11 | export function TabBarButton({ icon, title, onPress, focused }: any) {
12 | const color = focused ? ACTIVE_COLOR : INACTIVE_COLOR;
13 | const scale = useSharedValue(1);
14 |
15 | const animatedStyle = useAnimatedStyle(() => ({
16 | transform: [{ scale: scale.value }],
17 | }));
18 |
19 | return (
20 | {
23 | scale.value = withSpring(0.95);
24 | }}
25 | onPressOut={() => {
26 | scale.value = withSpring(1);
27 | }}
28 | style={[styles.pressable, animatedStyle]}
29 | >
30 | {icon({ focused, color })}
31 | {title}
32 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | pressable: {
38 | flex: 1,
39 | justifyContent: "center",
40 | alignItems: "center",
41 | },
42 | label: {
43 | marginTop: 2,
44 | fontSize: 11,
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | export const ACTIVE_COLOR = "#FFDD44";
2 | export const INACTIVE_COLOR = "#FFF9DB";
3 |
--------------------------------------------------------------------------------
/icons/ExploreIcon.tsx:
--------------------------------------------------------------------------------
1 | import Animated, {
2 | useAnimatedStyle,
3 | useDerivedValue,
4 | withSpring,
5 | } from "react-native-reanimated";
6 | import Svg, { Path } from "react-native-svg";
7 |
8 | interface Props {
9 | focused: boolean;
10 | color?: string;
11 | }
12 |
13 | export function ExploreIcon({ color = "#001A72", focused, ...rest }: Props) {
14 | const sv = useDerivedValue(() => {
15 | return focused ? withSpring(180) : 0;
16 | });
17 |
18 | const animatedStyle = useAnimatedStyle(() => ({
19 | transform: [{ rotate: `${sv.value}deg` }],
20 | }));
21 |
22 | return (
23 |
24 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/icons/HomeIcon.tsx:
--------------------------------------------------------------------------------
1 | import Svg, { Path } from "react-native-svg";
2 |
3 | interface Props {
4 | color?: string;
5 | focused: boolean;
6 | }
7 |
8 | export function HomeIcon({ color = "#001A72", focused, ...rest }: Props) {
9 | return (
10 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/icons/ProfileIcon.tsx:
--------------------------------------------------------------------------------
1 | import Animated, {
2 | useAnimatedStyle,
3 | useDerivedValue,
4 | withSpring,
5 | } from "react-native-reanimated";
6 | import Svg, { Path } from "react-native-svg";
7 |
8 | interface Props {
9 | focused: boolean;
10 | color?: string;
11 | }
12 |
13 | export function ProfileIcon({ color = "#001A72", focused, ...rest }: Props) {
14 | const sv = useDerivedValue(() => {
15 | return focused ? withSpring(360, { damping: 50 }) : 0;
16 | });
17 |
18 | const animatedStyle = useAnimatedStyle(() => ({
19 | transform: [{ rotateY: `${sv.value}deg` }],
20 | }));
21 |
22 | return (
23 |
24 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require('expo/metro-config');
3 |
4 | /** @type {import('expo/metro-config').MetroConfig} */
5 | const config = getDefaultConfig(__dirname, {
6 | // [Web-only]: Enables CSS support in Metro.
7 | isCSSEnabled: true,
8 | });
9 |
10 | module.exports = config;
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reanimated-expo-router-tabs",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios",
9 | "test": "jest --watchAll"
10 | },
11 | "jest": {
12 | "preset": "jest-expo"
13 | },
14 | "dependencies": {
15 | "@react-navigation/native": "^7.1.6",
16 | "expo": "^53.0.9",
17 | "expo-blur": "~14.1.4",
18 | "expo-image": "~2.1.7",
19 | "expo-linking": "~7.1.5",
20 | "expo-router": "~5.0.7",
21 | "expo-splash-screen": "~0.30.8",
22 | "expo-status-bar": "~2.2.3",
23 | "radon-ide": "^0.0.1",
24 | "react": "19.0.0",
25 | "react-native": "0.79.2",
26 | "react-native-gesture-handler": "~2.24.0",
27 | "react-native-reanimated": "~3.17.4",
28 | "react-native-safe-area-context": "5.4.0",
29 | "react-native-screens": "~4.10.0",
30 | "react-native-svg": "15.11.2",
31 | "react-native-svg-transformer": "^1.2.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.20.0",
35 | "@types/react": "~19.0.10",
36 | "jest": "^29.2.1",
37 | "jest-expo": "~53.0.5",
38 | "prettier": "^3.1.1",
39 | "typescript": "^5.1.3"
40 | },
41 | "overrides": {
42 | "react-refresh": "~0.14.0"
43 | },
44 | "resolutions": {
45 | "react-refresh": "~0.14.0"
46 | },
47 | "private": true
48 | }
49 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | },
6 | "include": [
7 | "**/*.ts",
8 | "**/*.tsx",
9 | ".expo/types/**/*.ts",
10 | "expo-env.d.ts"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------