Skip to content

Navigation

@symbiote-native/navigation is SymbioteNative’s navigation library — broadly comparable in scope to react-navigation, but built the SymbioteNative way: one shared, framework-agnostic core (packages/navigation/src/core/) drives thin React, Vue, Angular, and Svelte adapters, exactly like every other package in this monorepo. It ships native Stack navigation built on react-native-screensRNSScreen/RNSScreenStack views — real native push/pop transitions, headers, and modals, not a JS-only fake — plus pure-JS Tab and Drawer navigators (bottom tabs and a swipeable side panel) for the parts of a navigation surface that don’t need a native view underneath.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Terminal window
npm install @symbiote-native/navigation

The smallest possible working Stack: two screens and a push/pop button. Each screen reads its navigator handle with useStackNavigation() (or its Vue/Angular equivalent) — the narrowed handle for a Stack screen, so there’s no union and nothing to narrow by hand. See Core concepts below for the full set of hooks, composables, and injectors.

import { Stack, useStackNavigation } from '@symbiote-native/navigation/react';
import { Button } from '@symbiote-native/react';
function Home() {
const navigation = useStackNavigation();
return <Button title="Open details" onPress={() => navigation.push('Details')} />;
}
function Details() {
const navigation = useStackNavigation();
return <Button title="Go back" onPress={() => navigation.pop()} />;
}
export default function App() {
return (
<Stack initialRouteName="Home">
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Details" component={Details} />
</Stack>
);
}
  • Stack — the native stack navigator: headers, transitions, modals.
  • Tabs — the bottom tab bar navigator.
  • Drawer — the swipeable side-panel navigator.
  • HooksuseNavigation/useRoute/useIsFocused/useFocusEffect/useNavigationState (Svelte: the same names, as runes; Angular: their injectX equivalents) across all four adapters.
  • Linking — deep linking and navigation-state persistence.
  • FAQ — frequently asked questions.

There is no navigate(). Coming from react-navigation, the single biggest point of confusion is that every navigator here exposes a small imperative handle with explicit verbs instead: a Stack handle has push/pop/popToTop/popTo/replace/setParams/reset/ canGoBack; a Tab handle has jumpTo/setParams; a Drawer handle has openDrawer/ closeDrawer/toggleDrawer/jumpTo. There is no single navigate(name) that infers push vs. tab-switch vs. drawer-open depending on context — you call the verb that matches what you mean.

Screens are registered the way each framework already registers things, not forced into one shared shape. React uses JSX children (<Stack.Screen name="..." component={...} />); Vue uses default-slot components (<Screen name="..." :component="..." />, or a scoped slot for Drawer’s custom content); Angular uses structural marker directives on an inert ng-template (<ng-template symbioteScreen name="..." [component]="...">); Svelte uses the same declarative <Screen name="..." component={...} /> marker as Vue, but discovers it differently underneath — Svelte hands a component its children as an opaque Snippet, so <Screen>/<Stack.Screen> registers itself on a context-based collector while the snippet renders, rather than being read from a children scan. This is deliberate: each adapter follows its own framework’s idiom for declaring children rather than forcing one JSX-shaped convention onto Vue’s, Angular’s, or Svelte’s template model.

Read route/navigation with a hook — there is no props path. Every component under a navigator reads its current route and navigator handle by calling a hook (React/Vue), a rune (Svelte), or an inject function (Angular): useRoute()/useNavigation() — or, better, the navigator-specific useStackNavigation()/useTabNavigation()/useDrawerNavigation() (Svelte uses these same names; Angular: injectRoute()/injectNavigation()/injectStackNavigation()/…). The navigator does not thread route/navigation into the mounted screen as a prop or @Input(); the screen’s own top-level component reads them exactly the way any component nested deeper does, so there’s nothing to forward through the layers in between. useIsFocused()/useFocusEffect()/ useNavigationState() (Angular: injectIsFocused()/injectFocusEffect()/injectNavigationState()) are read the same way. See Hooks & focus for the full set.

Route params are unknown in this v1 — there is no per-navigator generic param-list type the way react-navigation’s RootStackParamList gives you. route.params always types as unknown; narrow it yourself (a type guard, a schema, a cast at the one I/O edge) on the screen that reads it.

All of the router logic — the route-stack reducer, screen-options resolution/merging, and the derivation of the native view props RNSScreen/RNSScreenStack/the header config leaves need — lives once in core/, shared verbatim by React, Vue, Angular, and Svelte. Each adapter supplies only its own lifecycle glue (hooks, composables, runes, or a directive/signal pair) and the descriptor bridge that turns the core’s render output into that framework’s elements:

packages/navigation/src/
├── core/ # framework-agnostic: reducers, screen-options resolution, render-stack/-tabs/-drawer, linking-config
├── register.ts # side-effect: registers react-native-screens' native ViewConfigs
├── react/ # React lifecycle (hooks) + descriptorToReact bridge
├── vue/ # Vue lifecycle (composables) + descriptorToVue bridge
├── angular/ # Angular lifecycle (injectors/signals) + directive-based descriptor bridge
└── svelte/ # Svelte lifecycle (runes) + descriptor-subtree bridge for Tab's pure-JS bar

A persistence bug in the route-stack reducer, or a header-layout bug in the native prop derivation, is fixed once in core/ for all four adapters — the same shared-core pattern every SymbioteNative package follows. See how it works for the general render-pipeline this sits on top of.