Skip to content

Commit adb4bf1

Browse files
committed
fix(android): add proper icons for native bottom tabs on Android
- Add react-native-bottom-tabs plugin to app.config.ts for Material 3 styling - Install react-native-vector-icons for synchronous icon loading on Android - Use Ionicons.getImageSourceSync() to pre-load Android tab bar icons - iOS continues to use SF Symbols, Android now uses Ionicons - Update tabBarIcon to return platform-appropriate icon format
1 parent 5c73e9f commit adb4bf1

4 files changed

Lines changed: 93 additions & 6 deletions

File tree

app.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
159159
},
160160
],
161161
'@react-native-firebase/app',
162+
'react-native-bottom-tabs', // Material 3 styling for native Android bottom tabs
162163
],
163164
experiments: {
164165
typedRoutes: true,

app/(tabs)/_layout.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React from 'react';
55
import { Platform } from 'react-native';
66
import { Tabs } from 'expo-router';
77
import { Home, BookOpen, TrendingUp, CheckSquare, User } from 'lucide-react-native';
8+
import Ionicons from 'react-native-vector-icons/Ionicons';
89
import { useTheme } from '@/contexts/ThemeContext';
910
import type { SFSymbol } from 'sf-symbols-typescript';
1011
import WebTopNav from '@/components/navigation/WebTopNav';
@@ -15,52 +16,71 @@ const NativeTabs =
1516
// eslint-disable-next-line @typescript-eslint/no-require-imports
1617
Platform.OS !== 'web' ? require('@/components/navigation/NativeBottomTabs').NativeTabs : null;
1718

19+
// Pre-load Android icons using Ionicons (only on Android)
20+
// These are loaded synchronously to avoid flickering during navigation
21+
const androidIcons =
22+
Platform.OS === 'android'
23+
? {
24+
home: Ionicons.getImageSourceSync('home', 24),
25+
book: Ionicons.getImageSourceSync('book', 24),
26+
trending: Ionicons.getImageSourceSync('trending-up', 24),
27+
tasks: Ionicons.getImageSourceSync('checkbox', 24),
28+
profile: Ionicons.getImageSourceSync('person', 24),
29+
}
30+
: null;
31+
1832
// =============================================================================
1933
// Types & Interfaces
2034
// =============================================================================
2135

22-
/** Tab route configuration with SF Symbols and Lucide fallbacks */
36+
/** Tab route configuration with SF Symbols (iOS) and Ionicons (Android) */
2337
interface TabRoute {
2438
name: string;
2539
title: string;
2640
sfSymbol: SFSymbol;
41+
androidIconKey: keyof NonNullable<typeof androidIcons>;
2742
icon: React.ComponentType<{ size?: number; color?: string }>;
2843
}
2944

3045
// =============================================================================
3146
// Constants
3247
// =============================================================================
3348

34-
/** Tab route configuration with SF Symbols and Lucide fallbacks */
49+
/** Tab route configuration with SF Symbols (iOS) and Ionicons (Android) */
3550
const tabRoutes: TabRoute[] = [
3651
{
3752
name: 'index',
3853
title: 'Home',
3954
sfSymbol: 'house.fill',
55+
androidIconKey: 'home',
4056
icon: Home,
4157
},
4258
{
4359
name: 'steps',
4460
title: 'Steps',
4561
sfSymbol: 'book.fill',
62+
androidIconKey: 'book',
4663
icon: BookOpen,
4764
},
4865
{
4966
name: 'journey',
5067
title: 'Journey',
5168
sfSymbol: 'chart.line.uptrend.xyaxis',
69+
androidIconKey: 'trending',
5270
icon: TrendingUp,
5371
},
5472
{
5573
name: 'tasks',
5674
title: 'Tasks',
5775
sfSymbol: 'checklist',
76+
androidIconKey: 'tasks',
5877
icon: CheckSquare,
5978
},
6079
{
6180
name: 'profile',
6281
title: 'Profile',
6382
sfSymbol: 'person.fill',
83+
androidIconKey: 'profile',
6484
icon: User,
6585
},
6686
];
@@ -127,7 +147,11 @@ export default function TabLayout(): React.ReactElement {
127147
name={route.name}
128148
options={{
129149
title: route.title,
130-
tabBarIcon: () => ({ sfSymbol: route.sfSymbol }),
150+
// iOS uses SF Symbols, Android uses pre-loaded Ionicons
151+
tabBarIcon: () =>
152+
Platform.OS === 'ios'
153+
? { sfSymbol: route.sfSymbol }
154+
: androidIcons![route.androidIconKey],
131155
}}
132156
/>
133157
))}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"react-native-screens": "~4.16.0",
8080
"react-native-svg": "^15.12.1",
8181
"react-native-url-polyfill": "^3.0.0",
82+
"react-native-vector-icons": "^10.3.0",
8283
"react-native-web": "~0.21.0",
8384
"react-native-worklets": "0.5.1",
8485
"sf-symbols-typescript": "^2.2.0"
@@ -87,6 +88,7 @@
8788
"@testing-library/react-native": "^13.3.3",
8889
"@types/jest": "^29.5.14",
8990
"@types/react": "~19.1.17",
91+
"@types/react-native-vector-icons": "^6.4.18",
9092
"babel-jest": "^29.7.0",
9193
"babel-preset-expo": "^54.0.8",
9294
"eslint": "^9.25.0",

pnpm-lock.yaml

Lines changed: 63 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)