├── .gitignore
├── README.md
├── app.json
├── app
├── (tabs)
│ ├── _layout.tsx
│ ├── explore.tsx
│ └── index.tsx
├── +html.tsx
├── +not-found.tsx
└── _layout.tsx
├── assets
├── fonts
│ └── SpaceMono-Regular.ttf
└── images
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ ├── partial-react-logo.png
│ ├── react-logo.png
│ ├── react-logo@2x.png
│ ├── react-logo@3x.png
│ └── splash.png
├── babel.config.js
├── bun.lockb
├── components
├── Collapsible.tsx
├── ExternalLink.tsx
├── HelloWave.tsx
├── ParallaxScrollView.tsx
├── ThemedText.tsx
├── ThemedView.tsx
├── __tests__
│ ├── ThemedText-test.tsx
│ └── __snapshots__
│ │ └── ThemedText-test.tsx.snap
├── bottom-tabs.tsx
└── navigation
│ └── TabBarIcon.tsx
├── constants
└── Colors.ts
├── hooks
├── useColorScheme.ts
├── useColorScheme.web.ts
└── useThemeColor.ts
├── package.json
├── scripts
└── reset-project.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
17 | # The following patterns were generated by expo-cli
18 |
19 | expo-env.d.ts
20 | # @end expo-cli
21 |
22 | /ios
23 | /android
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Expo Router + Native bottom tabs
2 |
3 | > This project requires a custom dev client and does not run in Expo Go.
4 |
5 | This is an example of using [`react-native-bottom-tabs`](https://github.com/okwasniewski/react-native-bottom-tabs) with Expo Router to use Apple's built-in native bottom tabs component. The minimum iOS version had to be bumped to 14.0 (which will be the default in SDK 52).
6 |
7 | First, create a custom layout adapter for the native bottom tabs:
8 |
9 | ```js
10 | import { withLayoutContext } from "expo-router";
11 | import { createNativeBottomTabNavigator } from "react-native-bottom-tabs/react-navigation";
12 |
13 | export const Tabs = withLayoutContext(
14 | createNativeBottomTabNavigator().Navigator
15 | );
16 | ```
17 |
18 | Then, use the `Tabs` component in your app:
19 |
20 | ```js
21 | import { Tabs } from "@/components/bottom-tabs";
22 |
23 | export default function TabLayout() {
24 | return (
25 |
26 | ({ sfSymbol: "house" }),
31 | }}
32 | />
33 | ({ sfSymbol: "person" }),
38 | }}
39 | />
40 |
41 | );
42 | }
43 | ```
44 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "oct8-tabs",
4 | "slug": "oct8-tabs",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "ios": {
16 | "supportsTablet": true,
17 | "bundleIdentifier": "com.bacon.oct8-tabs"
18 | },
19 | "android": {
20 | "adaptiveIcon": {
21 | "foregroundImage": "./assets/images/adaptive-icon.png",
22 | "backgroundColor": "#ffffff"
23 | }
24 | },
25 | "web": {
26 | "bundler": "metro",
27 | "output": "static",
28 | "favicon": "./assets/images/favicon.png"
29 | },
30 | "plugins": [
31 | "expo-router",
32 | [
33 | "expo-build-properties",
34 | {
35 | "ios": {
36 | "deploymentTarget": "14.0"
37 | }
38 | }
39 | ]
40 | ],
41 | "experiments": {
42 | "typedRoutes": true
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from "@/components/bottom-tabs";
2 |
3 | export default function TabLayout() {
4 | return (
5 |
6 | ({ sfSymbol: "house" }),
11 | }}
12 | />
13 | ({ sfSymbol: "person" }),
18 | }}
19 | />
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/app/(tabs)/explore.tsx:
--------------------------------------------------------------------------------
1 | import Ionicons from '@expo/vector-icons/Ionicons';
2 | import { StyleSheet, Image, Platform } from 'react-native';
3 |
4 | import { Collapsible } from '@/components/Collapsible';
5 | import { ExternalLink } from '@/components/ExternalLink';
6 | import ParallaxScrollView from '@/components/ParallaxScrollView';
7 | import { ThemedText } from '@/components/ThemedText';
8 | import { ThemedView } from '@/components/ThemedView';
9 |
10 | export default function TabTwoScreen() {
11 | return (
12 | }>
15 |
16 | Explore
17 |
18 | This app includes example code to help you get started.
19 |
20 |
21 | This app has two screens:{' '}
22 | app/(tabs)/index.tsx and{' '}
23 | app/(tabs)/explore.tsx
24 |
25 |
26 | The layout file in app/(tabs)/_layout.tsx{' '}
27 | sets up the tab navigator.
28 |
29 |
30 | Learn more
31 |
32 |
33 |
34 |
35 | You can open this project on Android, iOS, and the web. To open the web version, press{' '}
36 | w in the terminal running this project.
37 |
38 |
39 |
40 |
41 | For static images, you can use the @2x and{' '}
42 | @3x suffixes to provide files for
43 | different screen densities
44 |
45 |
46 |
47 | Learn more
48 |
49 |
50 |
51 |
52 | Open app/_layout.tsx to see how to load{' '}
53 |
54 | custom fonts such as this one.
55 |
56 |
57 |
58 | Learn more
59 |
60 |
61 |
62 |
63 | This template has light and dark mode support. The{' '}
64 | useColorScheme() hook lets you inspect
65 | what the user's current color scheme is, and so you can adjust UI colors accordingly.
66 |
67 |
68 | Learn more
69 |
70 |
71 |
72 |
73 | This template includes an example of an animated component. The{' '}
74 | components/HelloWave.tsx component uses
75 | the powerful react-native-reanimated library
76 | to create a waving hand animation.
77 |
78 | {Platform.select({
79 | ios: (
80 |
81 | The components/ParallaxScrollView.tsx{' '}
82 | component provides a parallax effect for the header image.
83 |
84 | ),
85 | })}
86 |
87 |
88 | );
89 | }
90 |
91 | const styles = StyleSheet.create({
92 | headerImage: {
93 | color: '#808080',
94 | bottom: -90,
95 | left: -35,
96 | position: 'absolute',
97 | },
98 | titleContainer: {
99 | flexDirection: 'row',
100 | gap: 8,
101 | },
102 | });
103 |
--------------------------------------------------------------------------------
/app/(tabs)/index.tsx:
--------------------------------------------------------------------------------
1 | import { Image, StyleSheet, Platform } from 'react-native';
2 |
3 | import { HelloWave } from '@/components/HelloWave';
4 | import ParallaxScrollView from '@/components/ParallaxScrollView';
5 | import { ThemedText } from '@/components/ThemedText';
6 | import { ThemedView } from '@/components/ThemedView';
7 |
8 | export default function HomeScreen() {
9 | return (
10 |
17 | }>
18 |
19 | Welcome!
20 |
21 |
22 |
23 | Step 1: Try it
24 |
25 | Edit app/(tabs)/index.tsx to see changes.
26 | Press{' '}
27 |
28 | {Platform.select({ ios: 'cmd + d', android: 'cmd + m' })}
29 | {' '}
30 | to open developer tools.
31 |
32 |
33 |
34 | Step 2: Explore
35 |
36 | Tap the Explore tab to learn more about what's included in this starter app.
37 |
38 |
39 |
40 | Step 3: Get a fresh start
41 |
42 | When you're ready, run{' '}
43 | npm run reset-project to get a fresh{' '}
44 | app directory. This will move the current{' '}
45 | app to{' '}
46 | app-example.
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | const styles = StyleSheet.create({
54 | titleContainer: {
55 | flexDirection: 'row',
56 | alignItems: 'center',
57 | gap: 8,
58 | },
59 | stepContainer: {
60 | gap: 8,
61 | marginBottom: 8,
62 | },
63 | reactLogo: {
64 | height: 178,
65 | width: 290,
66 | bottom: 0,
67 | left: 0,
68 | position: 'absolute',
69 | },
70 | });
71 |
--------------------------------------------------------------------------------
/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from 'expo-router/html';
2 | import { type PropsWithChildren } from 'react';
3 |
4 | /**
5 | * This file is web-only and used to configure the root HTML for every web page during static rendering.
6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
7 | */
8 | export default function Root({ children }: PropsWithChildren) {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | {/*
17 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
18 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
19 | */}
20 |
21 |
22 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
23 |
24 | {/* Add any additional elements that you want globally available on web... */}
25 |
26 | {children}
27 |
28 | );
29 | }
30 |
31 | const responsiveBackground = `
32 | body {
33 | background-color: #fff;
34 | }
35 | @media (prefers-color-scheme: dark) {
36 | body {
37 | background-color: #000;
38 | }
39 | }`;
40 |
--------------------------------------------------------------------------------
/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from 'expo-router';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import { ThemedText } from '@/components/ThemedText';
5 | import { ThemedView } from '@/components/ThemedView';
6 |
7 | export default function NotFoundScreen() {
8 | return (
9 | <>
10 |
11 |
12 | This screen doesn't exist.
13 |
14 | Go to home screen!
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: 'center',
25 | justifyContent: 'center',
26 | padding: 20,
27 | },
28 | link: {
29 | marginTop: 15,
30 | paddingVertical: 15,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
2 | import { useFonts } from 'expo-font';
3 | import { Stack } from 'expo-router';
4 | import * as SplashScreen from 'expo-splash-screen';
5 | import { useEffect } from 'react';
6 | import 'react-native-reanimated';
7 |
8 | import { useColorScheme } from '@/hooks/useColorScheme';
9 |
10 | // Prevent the splash screen from auto-hiding before asset loading is complete.
11 | SplashScreen.preventAutoHideAsync();
12 |
13 | export default function RootLayout() {
14 | const colorScheme = useColorScheme();
15 | const [loaded] = useFonts({
16 | SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
17 | });
18 |
19 | useEffect(() => {
20 | if (loaded) {
21 | SplashScreen.hideAsync();
22 | }
23 | }, [loaded]);
24 |
25 | if (!loaded) {
26 | return null;
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/assets/images/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EvanBacon/expo-router-react-native-bottom-tabs/075dce0d9f85cd931bce03c39f9d09c1bd0779fb/bun.lockb
--------------------------------------------------------------------------------
/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import Ionicons from '@expo/vector-icons/Ionicons';
2 | import { PropsWithChildren, useState } from 'react';
3 | import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native';
4 |
5 | import { ThemedText } from '@/components/ThemedText';
6 | import { ThemedView } from '@/components/ThemedView';
7 | import { Colors } from '@/constants/Colors';
8 |
9 | export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
10 | const [isOpen, setIsOpen] = useState(false);
11 | const theme = useColorScheme() ?? 'light';
12 |
13 | return (
14 |
15 | setIsOpen((value) => !value)}
18 | activeOpacity={0.8}>
19 |
24 | {title}
25 |
26 | {isOpen && {children}}
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | heading: {
33 | flexDirection: 'row',
34 | alignItems: 'center',
35 | gap: 6,
36 | },
37 | content: {
38 | marginTop: 6,
39 | marginLeft: 24,
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'expo-router';
2 | import { openBrowserAsync } from 'expo-web-browser';
3 | import { type ComponentProps } from 'react';
4 | import { Platform } from 'react-native';
5 |
6 | type Props = Omit, 'href'> & { href: string };
7 |
8 | export function ExternalLink({ href, ...rest }: Props) {
9 | return (
10 | {
15 | if (Platform.OS !== 'web') {
16 | // Prevent the default behavior of linking to the default browser on native.
17 | event.preventDefault();
18 | // Open the link in an in-app browser.
19 | await openBrowserAsync(href);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/HelloWave.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import Animated, {
3 | useSharedValue,
4 | useAnimatedStyle,
5 | withTiming,
6 | withRepeat,
7 | withSequence,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedText } from '@/components/ThemedText';
11 |
12 | export function HelloWave() {
13 | const rotationAnimation = useSharedValue(0);
14 |
15 | rotationAnimation.value = withRepeat(
16 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
17 | 4 // Run the animation 4 times
18 | );
19 |
20 | const animatedStyle = useAnimatedStyle(() => ({
21 | transform: [{ rotate: `${rotationAnimation.value}deg` }],
22 | }));
23 |
24 | return (
25 |
26 | 👋
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | text: {
33 | fontSize: 28,
34 | lineHeight: 32,
35 | marginTop: -6,
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from 'react';
2 | import { StyleSheet, useColorScheme } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedRef,
6 | useAnimatedStyle,
7 | useScrollViewOffset,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedView } from '@/components/ThemedView';
11 |
12 | const HEADER_HEIGHT = 250;
13 |
14 | type Props = PropsWithChildren<{
15 | headerImage: ReactElement;
16 | headerBackgroundColor: { dark: string; light: string };
17 | }>;
18 |
19 | export default function ParallaxScrollView({
20 | children,
21 | headerImage,
22 | headerBackgroundColor,
23 | }: Props) {
24 | const colorScheme = useColorScheme() ?? 'light';
25 | const scrollRef = useAnimatedRef();
26 | const scrollOffset = useScrollViewOffset(scrollRef);
27 |
28 | const headerAnimatedStyle = useAnimatedStyle(() => {
29 | return {
30 | transform: [
31 | {
32 | translateY: interpolate(
33 | scrollOffset.value,
34 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
35 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
36 | ),
37 | },
38 | {
39 | scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
40 | },
41 | ],
42 | };
43 | });
44 |
45 | return (
46 |
47 |
48 |
54 | {headerImage}
55 |
56 | {children}
57 |
58 |
59 | );
60 | }
61 |
62 | const styles = StyleSheet.create({
63 | container: {
64 | flex: 1,
65 | },
66 | header: {
67 | height: 250,
68 | overflow: 'hidden',
69 | },
70 | content: {
71 | flex: 1,
72 | padding: 32,
73 | gap: 16,
74 | overflow: 'hidden',
75 | },
76 | });
77 |
--------------------------------------------------------------------------------
/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { Text, type TextProps, StyleSheet } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
9 | };
10 |
11 | export function ThemedText({
12 | style,
13 | lightColor,
14 | darkColor,
15 | type = 'default',
16 | ...rest
17 | }: ThemedTextProps) {
18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19 |
20 | return (
21 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | default: {
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | defaultSemiBold: {
42 | fontSize: 16,
43 | lineHeight: 24,
44 | fontWeight: '600',
45 | },
46 | title: {
47 | fontSize: 32,
48 | fontWeight: 'bold',
49 | lineHeight: 32,
50 | },
51 | subtitle: {
52 | fontSize: 20,
53 | fontWeight: 'bold',
54 | },
55 | link: {
56 | lineHeight: 30,
57 | fontSize: 16,
58 | color: '#0a7ea4',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedViewProps = ViewProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | };
9 |
10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12 |
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/components/__tests__/ThemedText-test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { ThemedText } from '../ThemedText';
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer.create(Snapshot test!).toJSON();
8 |
9 | expect(tree).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/ThemedText-test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders correctly 1`] = `
4 |
22 | Snapshot test!
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/components/bottom-tabs.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createNativeBottomTabNavigator,
3 | // BottomSheetNavigationOptions,
4 | } from "react-native-bottom-tabs/react-navigation";
5 |
6 | import { withLayoutContext } from "expo-router";
7 |
8 | const { Navigator } = createNativeBottomTabNavigator();
9 |
10 | export const Tabs = withLayoutContext(
11 | Navigator
12 | );
13 |
--------------------------------------------------------------------------------
/components/navigation/TabBarIcon.tsx:
--------------------------------------------------------------------------------
1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
2 |
3 | import Ionicons from '@expo/vector-icons/Ionicons';
4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet';
5 | import { type ComponentProps } from 'react';
6 |
7 | export function TabBarIcon({ style, ...rest }: IconProps['name']>) {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = '#0a7ea4';
7 | const tintColorDark = '#fff';
8 |
9 | export const Colors = {
10 | light: {
11 | text: '#11181C',
12 | background: '#fff',
13 | tint: tintColorLight,
14 | icon: '#687076',
15 | tabIconDefault: '#687076',
16 | tabIconSelected: tintColorLight,
17 | },
18 | dark: {
19 | text: '#ECEDEE',
20 | background: '#151718',
21 | tint: tintColorDark,
22 | icon: '#9BA1A6',
23 | tabIconDefault: '#9BA1A6',
24 | tabIconSelected: tintColorDark,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // NOTE: The default React Native styling doesn't support server rendering.
2 | // Server rendered styles should not change between the first render of the HTML
3 | // and the first render on the client. Typically, web developers will use CSS media queries
4 | // to render different styles on the client and server, these aren't directly supported in React Native
5 | // but can be achieved using a styling library like Nativewind.
6 | export function useColorScheme() {
7 | return 'light';
8 | }
9 |
--------------------------------------------------------------------------------
/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { useColorScheme } from 'react-native';
7 |
8 | import { Colors } from '@/constants/Colors';
9 |
10 | export function useThemeColor(
11 | props: { light?: string; dark?: string },
12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
13 | ) {
14 | const theme = useColorScheme() ?? 'light';
15 | const colorFromProps = props[theme];
16 |
17 | if (colorFromProps) {
18 | return colorFromProps;
19 | } else {
20 | return Colors[theme][colorName];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oct8-tabs",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo run:android",
9 | "ios": "expo run:ios",
10 | "web": "expo start --web",
11 | "test": "jest --watchAll",
12 | "lint": "expo lint"
13 | },
14 | "jest": {
15 | "preset": "jest-expo"
16 | },
17 | "dependencies": {
18 | "@expo/vector-icons": "^14.0.2",
19 | "@react-navigation/native": "^6.0.2",
20 | "expo": "~51.0.28",
21 | "expo-build-properties": "~0.12.5",
22 | "expo-constants": "~16.0.2",
23 | "expo-font": "~12.0.9",
24 | "expo-linking": "~6.3.1",
25 | "expo-router": "~3.5.23",
26 | "expo-splash-screen": "~0.27.5",
27 | "expo-status-bar": "~1.12.1",
28 | "expo-system-ui": "~3.0.7",
29 | "expo-web-browser": "~13.0.3",
30 | "react": "18.2.0",
31 | "react-dom": "18.2.0",
32 | "react-native": "0.74.5",
33 | "react-native-bottom-tabs": "^0.0.7",
34 | "react-native-gesture-handler": "~2.16.1",
35 | "react-native-reanimated": "~3.10.1",
36 | "react-native-safe-area-context": "4.10.5",
37 | "react-native-screens": "3.31.1",
38 | "react-native-web": "~0.19.10"
39 | },
40 | "devDependencies": {
41 | "@babel/core": "^7.20.0",
42 | "@types/jest": "^29.5.12",
43 | "@types/react": "~18.2.45",
44 | "@types/react-test-renderer": "^18.0.7",
45 | "jest": "^29.2.1",
46 | "jest-expo": "~51.0.3",
47 | "react-test-renderer": "18.2.0",
48 | "typescript": "~5.3.3"
49 | },
50 | "private": true
51 | }
52 |
--------------------------------------------------------------------------------
/scripts/reset-project.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script is used to reset the project to a blank state.
5 | * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it.
7 | */
8 |
9 | const fs = require('fs');
10 | const path = require('path');
11 |
12 | const root = process.cwd();
13 | const oldDirPath = path.join(root, 'app');
14 | const newDirPath = path.join(root, 'app-example');
15 | const newAppDirPath = path.join(root, 'app');
16 |
17 | const indexContent = `import { Text, View } from "react-native";
18 |
19 | export default function Index() {
20 | return (
21 |
28 | Edit app/index.tsx to edit this screen.
29 |
30 | );
31 | }
32 | `;
33 |
34 | const layoutContent = `import { Stack } from "expo-router";
35 |
36 | export default function RootLayout() {
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 | `;
44 |
45 | fs.rename(oldDirPath, newDirPath, (error) => {
46 | if (error) {
47 | return console.error(`Error renaming directory: ${error}`);
48 | }
49 | console.log('/app moved to /app-example.');
50 |
51 | fs.mkdir(newAppDirPath, { recursive: true }, (error) => {
52 | if (error) {
53 | return console.error(`Error creating new app directory: ${error}`);
54 | }
55 | console.log('New /app directory created.');
56 |
57 | const indexPath = path.join(newAppDirPath, 'index.tsx');
58 | fs.writeFile(indexPath, indexContent, (error) => {
59 | if (error) {
60 | return console.error(`Error creating index.tsx: ${error}`);
61 | }
62 | console.log('app/index.tsx created.');
63 |
64 | const layoutPath = path.join(newAppDirPath, '_layout.tsx');
65 | fs.writeFile(layoutPath, layoutContent, (error) => {
66 | if (error) {
67 | return console.error(`Error creating _layout.tsx: ${error}`);
68 | }
69 | console.log('app/_layout.tsx created.');
70 | });
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "**/*.ts",
13 | "**/*.tsx",
14 | ".expo/types/**/*.ts",
15 | "expo-env.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------