A headless tabs system that provides complete control over tab navigation UI while integrating with One's file-based routing.
Unlike the standard <Tabs /> component which uses @react-navigation/bottom-tabs with opinionated styling, these components are completely unstyled - you bring your own design.
import { Tabs, TabList, TabTrigger, TabSlot } from 'one/ui'
| Component | Description |
|-----------|-------------|
| Tabs | Root container managing navigation state |
| TabList | Container for tab triggers (the tab bar) |
| TabTrigger | Pressable that switches between tabs |
| TabSlot | Renders the active tab's content |
import { Tabs, TabList, TabTrigger, TabSlot } from 'one/ui'
export default function Layout() {
return (
<Tabs style={{ flex: 1 }}>
<TabSlot />
<TabList style={styles.tabBar}>
<TabTrigger name="home" href="/">
<Text>Home</Text>
</TabTrigger>
<TabTrigger name="profile" href="/profile">
<Text>Profile</Text>
</TabTrigger>
</TabList>
</Tabs>
)
}
Use asChild to compose with your own button components:
<TabList style={styles.tabBar}>
<TabTrigger name="home" href="/" asChild resetOnFocus>
<CustomTabButton icon="home">Home</CustomTabButton>
</TabTrigger>
<TabTrigger name="search" href="/search" asChild>
<CustomTabButton icon="search">Search</CustomTabButton>
</TabTrigger>
</TabList>
Hook version of <Tabs> for custom wrapper components:
import { useTabsWithChildren } from 'one/ui'
export function MyTabs({ children }) {
const { NavigationContent } = useTabsWithChildren({ children })
return <NavigationContent />
}
For explicit trigger control without parsing children:
import { useTabsWithTriggers } from 'one/ui'
export function MyTabs() {
const { NavigationContent } = useTabsWithTriggers({
triggers: [
{ type: 'internal', name: 'home', href: '/' },
{ type: 'internal', name: 'profile', href: '/profile' },
]
})
return <NavigationContent />
}
Returns current tab element for custom slot rendering:
import { useTabSlot } from 'one/ui'
function MyTabSlot() {
const slot = useTabSlot()
return slot
}
Build fully custom tab triggers:
import { useTabTrigger } from 'one/ui'
function CustomTabBar() {
const home = useTabTrigger({ name: 'home' })
const profile = useTabTrigger({ name: 'profile' })
return (
<View style={styles.bar}>
<Pressable {...home.triggerProps}>
<Text style={home.trigger?.isFocused && styles.active}>
Home
</Text>
</Pressable>
<Pressable {...profile.triggerProps}>
<Text style={profile.trigger?.isFocused && styles.active}>
Profile
</Text>
</Pressable>
</View>
)
}
| Prop | Type | Description |
|------|------|-------------|
| name | string | Route name (required) |
| href | Href | Navigation target (required) |
| asChild | boolean | Compose with child component |
| resetOnFocus | boolean | Reset tab state when focused |
| Prop | Type | Description |
|------|------|-------------|
| detachInactiveScreens | boolean | Unmount inactive tabs |
| renderFn | function | Custom render function |
Use Headless Tabs when you need:
Use Standard Tabs (import { Tabs } from 'one') when you want:
Standard Tabs:
import { Tabs } from 'one'
<Tabs>
<Tabs.Screen name="home" options={{ title: 'Home' }} />
</Tabs>
Headless Tabs:
import { Tabs, TabList, TabTrigger, TabSlot } from 'one/ui'
<Tabs>
<TabSlot />
<TabList>
<TabTrigger name="home" href="/">Home</TabTrigger>
</TabList>
</Tabs>
Edit this page on GitHub.