diff --git a/apps/onestack.dev/data/docs/components-NativeTabs.mdx b/apps/onestack.dev/data/docs/components-NativeTabs.mdx
new file mode 100644
index 0000000000..b4e9ee46aa
--- /dev/null
+++ b/apps/onestack.dev/data/docs/components-NativeTabs.mdx
@@ -0,0 +1,106 @@
+---
+title: ""
+---
+
+The NativeTabs component provides a native platform tab bar using [`react-native-bottom-tabs`](https://github.com/okwasniewski/react-native-bottom-tabs). Unlike the regular [Tabs](/docs/components-Tabs) component which uses a JavaScript-based tab bar, NativeTabs renders using the platform's native tab bar (`UITabBarController` on iOS, Material Bottom Tabs on Android), giving you native feel and SF Symbol support on iOS.
+
+Import it from `one/native-tabs` to keep the bundle size minimal when not using native tabs.
+
+This component should only be rendered inside a `_layout.tsx` file, where it will serve as the location that children will render for routes below the layout.
+
+NativeTabs wraps the navigator from [`@bottom-tabs/react-navigation`](https://github.com/react-navigation/react-navigation) and accepts the same props.
+
+## Basic Usage
+
+```tsx fileName="_layout.native.tsx"
+import { NativeTabs } from 'one/native-tabs'
+
+export default function Layout() {
+ return (
+
+ ({ sfSymbol: 'house' }),
+ }}
+ />
+ ({ sfSymbol: 'person' }),
+ }}
+ />
+
+ )
+}
+```
+
+## Tab Icons
+
+On iOS, you can use [SF Symbols](https://developer.apple.com/sf-symbols/) by returning an object with `sfSymbol`:
+
+```tsx
+tabBarIcon: () => ({ sfSymbol: 'house' })
+```
+
+On Android, SF Symbols are not available. You can use imported SVG or image assets instead:
+
+```tsx fileName="_layout.native.tsx"
+import { Platform } from 'react-native'
+import { NativeTabs } from 'one/native-tabs'
+import homeIcon from '../assets/icons/home.svg'
+
+export default function Layout() {
+ return (
+
+
+ Platform.OS === 'ios' ? { sfSymbol: 'house' } : homeIcon,
+ }}
+ />
+
+ )
+}
+```
+
+
+ Use `import` for assets rather than `require()`.
+
+
+## Platform-Specific Layouts
+
+Since NativeTabs is native-only, you'll typically pair it with a web layout using [platform-specific file extensions](/docs/routing):
+
+```
+app/
+├── _layout.tsx # Web layout (Tabs or custom)
+├── _layout.native.tsx # Native layout (NativeTabs)
+├── index.tsx
+└── profile.tsx
+```
+
+## Common Options
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `title` | `string` | Tab label text |
+| `tabBarIcon` | `() => ImageSource \| { sfSymbol: string }` | Tab icon (SF Symbol on iOS, image asset on Android) |
+| `tabBarBadge` | `string` | Badge text on the tab |
+| `tabBarActiveTintColor` | `string` | Active tab tint color |
+
+## Dependencies
+
+NativeTabs requires these optional peer dependencies to be installed:
+
+```bash
+npm install @bottom-tabs/react-navigation react-native-bottom-tabs
+```
+
+These are marked as optional peer dependencies in `one`, so they won't be installed automatically. This keeps bundle sizes smaller for apps that don't use NativeTabs.
+
+For more configuration options, see the [`react-native-bottom-tabs` documentation](https://github.com/okwasniewski/react-native-bottom-tabs).
diff --git a/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx b/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx
index 7d15f436ba..aceecc80ab 100644
--- a/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx
+++ b/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx
@@ -3,11 +3,9 @@ title: withLayoutContext
description: Create your own layouts.
---
-Today there are a few built-in navigators: [Slot](/docs/components-Slot), [Stack](/docs/components-Stack), etc. But you may want to create your own.
+Today there are a few built-in navigators: [Slot](/docs/components-Slot), [Stack](/docs/components-Stack), [Tabs](/docs/components-Tabs), [NativeTabs](/docs/components-NativeTabs), and [Drawer](/docs/components-Drawer). But you may want to create your own.
-A good example of this is the new [`react-native-bottom-tabs`](https://github.com/okwasniewski/react-native-bottom-tabs) library, that exports a React Navigation compatible navigator.
-
-To make any React Navigation navigator work inside a One layout, use `withLayoutContext`:
+To make any React Navigation navigator work inside a One layout, use `withLayoutContext`. For example, this is how [NativeTabs](/docs/components-NativeTabs) is built:
```tsx
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'
@@ -20,4 +18,4 @@ export const NativeTabs = withLayoutContext(
)
```
-Now you can use `NativeTabs` in any `_layout.tsx` file.
+Now you can use `NativeTabs` in any `_layout.tsx` file. This same pattern works with any React Navigation compatible navigator.
diff --git a/apps/onestack.dev/data/docs/routing-layouts.mdx b/apps/onestack.dev/data/docs/routing-layouts.mdx
index 3f46a68447..ca9c820e7d 100644
--- a/apps/onestack.dev/data/docs/routing-layouts.mdx
+++ b/apps/onestack.dev/data/docs/routing-layouts.mdx
@@ -35,6 +35,7 @@ Layouts must render one of the following to show the matched pages that exist in
- [Slot](/docs/components-Slot): Directly show sub-route with no frame.
- [Stack](/docs/components-Stack): Creates a [React Navigation Stack](https://reactnavigation.org/docs/stack-navigator/) of sub-routes.
- [Tabs](/docs/components-Tabs): Creates a [React Navigation BottomTabs](https://reactnavigation.org/docs/bottom-tab-navigator) with sub-routes.
+- [NativeTabs](/docs/components-NativeTabs): Creates a native platform tab bar using [react-native-bottom-tabs](https://github.com/okwasniewski/react-native-bottom-tabs) with sub-routes.
- [Drawer](/docs/components-Drawer): Creates a [React Navigation Drawer](https://reactnavigation.org/docs/drawer-navigator) with sub-routes.
This looks something like this at the simplest:
diff --git a/apps/onestack.dev/features/docs/docsRoutes.tsx b/apps/onestack.dev/features/docs/docsRoutes.tsx
index 7667249fe4..c03bdcb918 100644
--- a/apps/onestack.dev/features/docs/docsRoutes.tsx
+++ b/apps/onestack.dev/features/docs/docsRoutes.tsx
@@ -41,6 +41,7 @@ export const docsRoutes = [
{ title: 'Slot', route: '/docs/components-Slot' },
{ title: 'Stack', route: '/docs/components-Stack' },
{ title: 'Tabs', route: '/docs/components-Tabs' },
+ { title: 'NativeTabs', route: '/docs/components-NativeTabs' },
{ title: 'Drawer', route: '/docs/components-Drawer' },
{ title: 'Protected', route: '/docs/components-Protected' },
{ title: 'withLayoutContext', route: '/docs/exports-withLayoutContext' },
diff --git a/bun.lock b/bun.lock
index 92ae67138e..a64dcbf8d3 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
- "configVersion": 0,
"workspaces": {
"": {
"name": "vxrn-monorepo",
@@ -395,6 +394,7 @@
"xxhashjs": "^0.2.2",
},
"devDependencies": {
+ "@bottom-tabs/react-navigation": "^0.9.1",
"@react-navigation/core": "^7.13.0",
"@react-navigation/drawer": "~7.7.2",
"@react-navigation/native": "~7.1.19",
@@ -405,6 +405,7 @@
"depcheck": "^1.4.7",
"immer": "^10.1.1",
"react-native": "0.81.5",
+ "react-native-bottom-tabs": "^1.0.2",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.3",
"rollup": "^4.29.1",
@@ -413,9 +414,11 @@
"vitest": "^4.0.6",
},
"peerDependencies": {
+ "@bottom-tabs/react-navigation": ">=0.9.0",
"@react-navigation/drawer": "~7.7.2",
"@react-navigation/native": "~7.1.0",
"react-native": "*",
+ "react-native-bottom-tabs": ">=1.0.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1",
@@ -423,8 +426,10 @@
"sharp": ">=0.33.0",
},
"optionalPeers": [
+ "@bottom-tabs/react-navigation",
"@react-navigation/drawer",
"react-native",
+ "react-native-bottom-tabs",
"react-native-gesture-handler",
"react-native-reanimated",
"sharp",
diff --git a/examples/testflight/assets/icons/bell.svg b/examples/testflight/assets/icons/bell.svg
new file mode 100644
index 0000000000..70ea70041e
--- /dev/null
+++ b/examples/testflight/assets/icons/bell.svg
@@ -0,0 +1 @@
+
diff --git a/examples/testflight/assets/icons/newspaper.svg b/examples/testflight/assets/icons/newspaper.svg
new file mode 100644
index 0000000000..06d87752c2
--- /dev/null
+++ b/examples/testflight/assets/icons/newspaper.svg
@@ -0,0 +1 @@
+
diff --git a/examples/testflight/assets/icons/user.svg b/examples/testflight/assets/icons/user.svg
new file mode 100644
index 0000000000..10588f7f10
--- /dev/null
+++ b/examples/testflight/assets/icons/user.svg
@@ -0,0 +1 @@
+
diff --git a/examples/testflight/code/home/HomeLayout.native.tsx b/examples/testflight/code/home/HomeLayout.native.tsx
index c3d0846262..2f40242108 100644
--- a/examples/testflight/code/home/HomeLayout.native.tsx
+++ b/examples/testflight/code/home/HomeLayout.native.tsx
@@ -1,4 +1,10 @@
+import { Platform } from 'react-native'
import { NativeTabs } from '~/code/ui/BottomTabs.native'
+import newspaperIcon from '../../assets/icons/newspaper.svg'
+import bellIcon from '../../assets/icons/bell.svg'
+import userIcon from '../../assets/icons/user.svg'
+
+const isIOS = Platform.OS === 'ios'
export function HomeLayout() {
return (
@@ -7,7 +13,8 @@ export function HomeLayout() {
name="index"
options={{
title: 'Feed',
- tabBarIcon: () => ({ sfSymbol: 'newspaper' }),
+ tabBarIcon: () =>
+ isIOS ? { sfSymbol: 'newspaper' } : newspaperIcon,
}}
/>
@@ -15,7 +22,8 @@ export function HomeLayout() {
name="notifications"
options={{
title: 'Notifications',
- tabBarIcon: () => ({ sfSymbol: 'bell' }),
+ tabBarIcon: () =>
+ isIOS ? { sfSymbol: 'bell' } : bellIcon,
}}
/>
@@ -23,7 +31,8 @@ export function HomeLayout() {
name="profile"
options={{
title: 'Profile',
- tabBarIcon: () => ({ sfSymbol: 'person' }),
+ tabBarIcon: () =>
+ isIOS ? { sfSymbol: 'person' } : userIcon,
}}
/>
diff --git a/examples/testflight/code/ui/BottomTabs.native.tsx b/examples/testflight/code/ui/BottomTabs.native.tsx
index 64d96a9445..ae1fe94038 100644
--- a/examples/testflight/code/ui/BottomTabs.native.tsx
+++ b/examples/testflight/code/ui/BottomTabs.native.tsx
@@ -1,4 +1 @@
-import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'
-import { withLayoutContext } from 'one'
-
-export const NativeTabs = withLayoutContext(createNativeBottomTabNavigator().Navigator)
+export { NativeTabs } from 'one/native-tabs'
diff --git a/packages/one/package.json b/packages/one/package.json
index eb17fb0180..ad3df28235 100644
--- a/packages/one/package.json
+++ b/packages/one/package.json
@@ -106,6 +106,15 @@
"import": "./dist/esm/drawer.mjs",
"require": "./dist/cjs/drawer.js"
},
+ "./native-tabs": {
+ "react-native": {
+ "import": "./dist/esm/native-tabs.native.js",
+ "require": "./dist/cjs/native-tabs.native.js"
+ },
+ "types": "./types/native-tabs.d.ts",
+ "import": "./dist/esm/native-tabs.mjs",
+ "require": "./dist/cjs/native-tabs.js"
+ },
"./devtools/*": "./devtools/*"
},
"main": "dist/cjs",
@@ -201,9 +210,17 @@
"react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.16.0",
+ "@bottom-tabs/react-navigation": ">=0.9.0",
+ "react-native-bottom-tabs": ">=1.0.0",
"sharp": ">=0.33.0"
},
"peerDependenciesMeta": {
+ "@bottom-tabs/react-navigation": {
+ "optional": true
+ },
+ "react-native-bottom-tabs": {
+ "optional": true
+ },
"@react-navigation/drawer": {
"optional": true
},
@@ -221,6 +238,8 @@
}
},
"devDependencies": {
+ "@bottom-tabs/react-navigation": "^0.9.1",
+ "react-native-bottom-tabs": "^1.0.2",
"@react-navigation/core": "^7.13.0",
"@react-navigation/drawer": "~7.7.2",
"@react-navigation/native": "~7.1.19",
diff --git a/packages/one/src/layouts/NativeTabs.tsx b/packages/one/src/layouts/NativeTabs.tsx
new file mode 100644
index 0000000000..c89bb8b9a9
--- /dev/null
+++ b/packages/one/src/layouts/NativeTabs.tsx
@@ -0,0 +1,24 @@
+import {
+ createNativeBottomTabNavigator,
+ type NativeBottomTabNavigationEventMap,
+ type NativeBottomTabNavigationOptions,
+} from '@bottom-tabs/react-navigation'
+import type { ParamListBase, TabNavigationState } from '@react-navigation/native'
+import type React from 'react'
+
+import { withLayoutContext } from './withLayoutContext'
+
+// typed as ComponentType because @bottom-tabs/react-navigation doesn't export
+// NativeBottomTabNavigatorProps, causing TS2742 on build. the actual typing comes
+// from the withLayoutContext generics below, not from the Navigator's own props.
+const NativeBottomTabNavigator: React.ComponentType =
+ createNativeBottomTabNavigator().Navigator
+
+export const NativeTabs = withLayoutContext<
+ NativeBottomTabNavigationOptions,
+ typeof NativeBottomTabNavigator,
+ TabNavigationState,
+ NativeBottomTabNavigationEventMap
+>(NativeBottomTabNavigator)
+
+export default NativeTabs
diff --git a/packages/one/src/native-tabs.ts b/packages/one/src/native-tabs.ts
new file mode 100644
index 0000000000..9c157ac44a
--- /dev/null
+++ b/packages/one/src/native-tabs.ts
@@ -0,0 +1 @@
+export { NativeTabs, NativeTabs as default } from './layouts/NativeTabs'
diff --git a/packages/one/types/layouts/NativeTabs.d.ts b/packages/one/types/layouts/NativeTabs.d.ts
new file mode 100644
index 0000000000..bf7a691b2a
--- /dev/null
+++ b/packages/one/types/layouts/NativeTabs.d.ts
@@ -0,0 +1,6 @@
+import type React from 'react';
+export declare const NativeTabs: React.ForwardRefExoticComponent, "ref"> & React.RefAttributes> & {
+ Screen: typeof import("../views/Screen").Screen;
+};
+export default NativeTabs;
+//# sourceMappingURL=NativeTabs.d.ts.map
\ No newline at end of file
diff --git a/packages/one/types/native-tabs.d.ts b/packages/one/types/native-tabs.d.ts
new file mode 100644
index 0000000000..6f3e27aa3c
--- /dev/null
+++ b/packages/one/types/native-tabs.d.ts
@@ -0,0 +1,2 @@
+export { NativeTabs, NativeTabs as default } from './layouts/NativeTabs';
+//# sourceMappingURL=native-tabs.d.ts.map
\ No newline at end of file