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-screens’ RNSScreen/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 |
Installation
Section titled “Installation”npm install @symbiote-native/navigationQuick start
Section titled “Quick start”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> );}<script setup lang="ts">import { defineComponent, h } from 'vue';import { Stack, Screen, useStackNavigation } from '@symbiote-native/navigation/vue';import { Button } from '@symbiote-native/vue';
const Home = defineComponent(() => { const navigation = useStackNavigation(); return () => h(Button, { title: 'Open details', onPress: () => navigation.value.push('Details') });});
const Details = defineComponent(() => { const navigation = useStackNavigation(); return () => h(Button, { title: 'Go back', onPress: () => navigation.value.pop() });});</script>
<template> <Stack initial-route-name="Home"> <Screen name="Home" :component="Home" /> <Screen name="Details" :component="Details" /> </Stack></template>import { Component } from '@angular/core';import { Stack, ScreenDirective, injectStackNavigation } from '@symbiote-native/navigation/angular';import { Button } from '@symbiote-native/angular';
@Component({ selector: 'Home', standalone: true, imports: [Button], template: `<Button title="Open details" (press)="navigation.push('Details')" />`,})class Home { protected readonly navigation = injectStackNavigation();}
@Component({ selector: 'Details', standalone: true, imports: [Button], template: `<Button title="Go back" (press)="navigation.pop()" />`,})class Details { protected readonly navigation = injectStackNavigation();}
@Component({ selector: 'App', standalone: true, imports: [Stack, ScreenDirective], template: ` <Stack initialRouteName="Home"> <ng-template symbioteScreen name="Home" [component]="home"></ng-template> <ng-template symbioteScreen name="Details" [component]="details"></ng-template> </Stack> `,})class App { readonly home = Home; readonly details = Details;}<script lang="ts"> import { Button } from '@symbiote-native/svelte'; import { useStackNavigation } from '@symbiote-native/navigation/svelte';
const navigation = useStackNavigation();</script>
<Button title="Open details" onPress={() => navigation.current.push('Details')} /><script lang="ts"> import { Button } from '@symbiote-native/svelte'; import { useStackNavigation } from '@symbiote-native/navigation/svelte';
const navigation = useStackNavigation();</script>
<Button title="Go back" onPress={() => navigation.current.pop()} /><script lang="ts"> import { Screen, Stack } from '@symbiote-native/navigation/svelte'; import Home from './Home.svelte'; import Details from './Details.svelte';</script>
<Stack initialRouteName="Home" ><Screen name="Home" component={Home} /><Screen name="Details" component={Details}/></Stack>What’s in this section
Section titled “What’s in this section”- Stack — the native stack navigator: headers, transitions, modals.
- Tabs — the bottom tab bar navigator.
- Drawer — the swipeable side-panel navigator.
- Hooks —
useNavigation/useRoute/useIsFocused/useFocusEffect/useNavigationState(Svelte: the same names, as runes; Angular: theirinjectXequivalents) across all four adapters. - Linking — deep linking and navigation-state persistence.
- FAQ — frequently asked questions.
Core concepts
Section titled “Core concepts”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.
How it works
Section titled “How it works”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 barA 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.