npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sigmela/router

v0.3.7

Published

React Native Router

Downloads

922

Readme

Sigmela Router (@sigmela/router)

Modern, lightweight navigation for React Native and Web, built on top of react-native-screens.

This library is URL-first: you navigate by paths (/users/42?tab=posts), and the router derives params and query for screens.

Features

  • Stacks: predictable stack-based navigation
  • Tabs: TabBar with native + web renderers (or custom tab bar)
  • Drawer: side-panel navigation (Drawer)
  • Split view: master/details navigation (SplitView)
  • Modals & sheets: via stackPresentation (modal, sheet, …)
  • Controllers: async/guarded navigation (only present when ready)
  • Appearance: global styling via NavigationAppearance (tab bar colors, fonts, blur effects, etc.)
  • Web History integration: keeps Router state in sync with pushState, replaceState, popstate
  • Dynamic root: swap root navigation tree at runtime (router.setRoot)
  • Type-safe hooks: useParams, useQueryParams, useRoute, useCurrentRoute

Installation

yarn add @sigmela/router react-native-screens
# optional (required only if you use sheet presentation)
yarn add @sigmela/native-sheet

Peer dependencies

  • react
  • react-native
  • react-native-screens (>= 4.24.0)
  • @sigmela/native-sheet (>= 0.0.1) — only if you use sheets

Web CSS

On web you must import the bundled stylesheet once:

import '@sigmela/router/styles.css';

Quick start

Simple stack

import {
  Navigation,
  NavigationStack,
  Router,
  useParams,
  useQueryParams,
  useRouter,
} from '@sigmela/router';

function HomeScreen() {
  const router = useRouter();
  return (
    <Button
      title="Open details"
      onPress={() => router.navigate('/details/42?from=home')}
    />
  );
}

function DetailsScreen() {
  const { id } = useParams<{ id: string }>();
  const { from } = useQueryParams<{ from?: string }>();
  return <Text>Details: id={id}, from={from ?? 'n/a'}</Text>;
}

const rootStack = new NavigationStack()
  .addScreen('/', HomeScreen, { header: { title: 'Home' } })
  .addScreen('/details/:id', DetailsScreen, { header: { title: 'Details' } });

const router = new Router({ roots: { app: rootStack }, root: 'app' });

export default function App() {
  return <Navigation router={router} />;
}

Tabs

import { Navigation, NavigationStack, Router, TabBar } from '@sigmela/router';

const homeStack = new NavigationStack().addScreen('/', HomeScreen);

const catalogStack = new NavigationStack()
  .addScreen('/catalog', CatalogScreen)
  .addScreen('/catalog/products/:productId', ProductScreen);

const tabBar = new TabBar({ initialIndex: 0 })
  .addTab({ key: 'home', stack: homeStack, title: 'Home' })
  .addTab({ key: 'catalog', stack: catalogStack, title: 'Catalog' });

const router = new Router({ roots: { app: tabBar }, root: 'app' });

export default function App() {
  return <Navigation router={router} />;
}

Tabs wrapped by a root stack (like example/src/navigation/stacks.ts)

In the example app, tabs are mounted as a screen inside a root NavigationStack. This lets you keep tab navigation plus define modals/overlays at the root level.

import { NavigationStack, TabBar } from '@sigmela/router';

const homeStack = new NavigationStack().addScreen('/', HomeScreen);
const catalogStack = new NavigationStack().addScreen('/catalog', CatalogScreen);

const tabBar = new TabBar()
  .addTab({
    key: 'home',
    stack: homeStack,
    title: 'Home',
    icon: require('./assets/home.png'),
  })
  .addTab({
    key: 'catalog',
    stack: catalogStack,
    title: 'Catalog',
    icon: require('./assets/catalog.png'),
  });

// Root stack hosts the tab bar AND top-level modals/overlays.
export const rootStack = new NavigationStack()
  .addScreen('/', tabBar)
  .addModal('/auth', AuthScreen, { header: { title: 'Login', hidden: true } })
  .addModal('*?modal=promo', PromoModal);

Core concepts

NavigationStack

A NavigationStack defines a set of routes and how to match them.

const stack = new NavigationStack({ header: { largeTitle: true } })
  .addScreen('/feed', FeedScreen)
  .addScreen('/feed/:id', FeedItemScreen)
  .addModal('/auth', AuthScreen)
  .addSheet('/settings', SettingsSheet);

Key methods:

  • addScreen(pathPattern, componentOrNode, options?)
  • addModal(pathPattern, componentOrStack, options?) (shorthand for stackPresentation: 'modal')
  • addSheet(pathPattern, componentOrStack, options?) (shorthand for stackPresentation: 'sheet')
  • addStack(prefixOrStack, maybeStack?) — compose nested stacks under a prefix

Provider Context

You can wrap an entire stack with a React context provider by passing a provider option:

import { ThemeProvider } from './theme';

const stack = new NavigationStack({
  header: { largeTitle: true },
  provider: ThemeProvider,
})
  .addScreen('/feed', FeedScreen)
  .addScreen('/feed/:id', FeedItemScreen);

The provider component wraps the entire stack renderer, making the context available to all screens in the stack. This is useful for:

  • Theme providers: Apply theme context to all screens
  • Auth providers: Share authentication state across screens
  • Localization: Provide i18n context to the entire stack

Composing multiple providers:

If you need multiple providers, create a composed component:

const ComposedProvider = ({ children }) => (
  <ThemeProvider>
    <AuthProvider>
      <I18nProvider>
        {children}
      </I18nProvider>
    </AuthProvider>
  </ThemeProvider>
);

const stack = new NavigationStack({ provider: ComposedProvider })
  .addScreen('/', HomeScreen);

Important: The provider should be a stable reference (not an inline arrow function) to avoid unnecessary re-renders.

Modal Stacks (Stack in Stack)

You can pass an entire NavigationStack to addModal() or addSheet() to create a multi-screen flow inside a modal:

// Define a flow with multiple screens
const emailVerifyStack = new NavigationStack()
  .addScreen('/verify', EmailInputScreen)
  .addScreen('/verify/sent', EmailSentScreen);

// Mount the entire stack as a modal
const rootStack = new NavigationStack()
  .addScreen('/', HomeScreen)
  .addModal('/verify', emailVerifyStack);

How it works:

  • Navigating to /verify opens the modal with EmailInputScreen
  • Inside the modal, router.navigate('/verify/sent') pushes EmailSentScreen within the same modal
  • router.goBack() navigates back inside the modal stack
  • router.dismiss() closes the entire modal from any depth

Example screen with navigation inside modal:

function EmailInputScreen() {
  const router = useRouter();
  
  return (
    <View>
      <Button title="Next" onPress={() => router.navigate('/verify/sent')} />
      <Button title="Close" onPress={() => router.dismiss()} />
    </View>
  );
}

function EmailSentScreen() {
  const router = useRouter();
  
  return (
    <View>
      <Button title="Back" onPress={() => router.goBack()} />
      <Button title="Done" onPress={() => router.dismiss()} />
    </View>
  );
}

This pattern works recursively — you can nest stacks inside stacks to any depth.

Router

The Router holds navigation state and performs path matching.

const router = new Router({
  roots: { app: root }, // NavigationNode (NavigationStack, TabBar, SplitView, ...)
  root: 'app',
  screenOptions, // optional defaults
  debug, // optional
});

Navigation:

  • router.navigate(path) — push
  • router.replace(path, dedupe?) — replace top of the active stack
  • router.goBack() — pop top of the active stack
  • router.dismiss() — close the nearest modal or sheet (including all screens in a modal stack)
  • router.reset(path)web-only: rebuild Router state as if app loaded at path
  • router.setRoot(rootKey, { transition? }) — swap root at runtime (rootKey from config.roots)

State/subscriptions:

  • router.getState(){ history: HistoryItem[] }
  • router.getActiveRoute()ActiveRoute | null
  • router.subscribe(cb) — notify on any history change
  • router.subscribeStack(stackId, cb) — notify when a particular stack slice changes
  • router.subscribeRoot(cb) — notify when root is replaced via setRoot
  • router.getStackHistory(stackId) — slice of history for a stack

TabBar

TabBar is a container node that renders one tab at a time.

const tabBar = new TabBar({ component: CustomTabBar, initialIndex: 0 })
  .addTab({ key: 'home', stack: homeStack, title: 'Home' })
  .addTab({ key: 'search', screen: SearchScreen, title: 'Search' });

Key methods:

  • addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, selectedIcon?, ... })
  • onIndexChange(index) — switch active tab
  • setBadge(index, badge | null)
  • setTabBarConfig(partialConfig)
  • getState() and subscribe(cb)

Notes:

  • Exactly one of stack, node, screen must be provided.
  • Use prefix to mount a tab's routes under a base path (e.g. /mail).
  • All TabsScreenProps from react-native-screens are forwarded to native. This includes lifecycle events (onWillAppear, onDidAppear, onWillDisappear, onDidDisappear), accessibility props (testID, accessibilityLabel, tabBarItemTestID, tabBarItemAccessibilityLabel), orientation, systemItem, freezeContents, placeholder, scrollEdgeEffects, badge styling, and more.

setTabBarConfig()

Runtime tab bar configuration:

tabBar.setTabBarConfig({
  bottomAccessory: (environment) => <MiniPlayer layout={environment} />,  // iOS 26+
  experimentalControlNavigationStateInJS: true,
});

Web behavior note:

  • The built-in web tab bar renderer resets Router history on tab switch (to keep URL and Router state consistent) using router.reset(firstRoutePath).

Drawer

Drawer provides side-panel navigation, similar to TabBar but with a slide-out panel.

import { Drawer, NavigationStack } from '@sigmela/router';

const homeStack = new NavigationStack().addScreen('/', HomeScreen);
const settingsStack = new NavigationStack().addScreen('/settings', SettingsScreen);

const drawer = new Drawer({ width: 280 })
  .addTab({ key: 'home', stack: homeStack, title: 'Home' })
  .addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });

Key methods:

  • addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, ... })
  • open(), close(), toggle() — manage drawer state
  • getIsOpen() — current open state
  • subscribeOpenState(listener) — subscribe to open/close changes
  • onIndexChange(index) — switch active tab
  • setBadge(index, badge | null)

SplitView

SplitView renders two stacks: primary and secondary.

  • On native, secondary overlays primary when it has at least one screen in its history.
  • On web, the layout becomes side-by-side at a fixed breakpoint (minWidth, default 640px).
import { NavigationStack, SplitView, TabBar } from '@sigmela/router';

const master = new NavigationStack().addScreen('/', ThreadsScreen);
const detail = new NavigationStack().addScreen('/:threadId', ThreadScreen);

const splitView = new SplitView({
  minWidth: 640,
  primary: master,
  secondary: detail,
  primaryMaxWidth: 390,
});

// Mount SplitView directly as a tab (no wrapper stack needed).
const tabBar = new TabBar()
  .addTab({ key: 'mail', node: splitView, prefix: '/mail', title: 'Mail' })
  .addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });

Controllers

Controllers let you delay/guard navigation. A route can be registered as:

import { createController } from '@sigmela/router';

const UserDetails = {
  component: UserDetailsScreen,
  controller: createController<{ userId: string }, { tab?: string }>(
    async ({ params, query }, present) => {
      const ok = await checkAuth();
      if (!ok) {
        router.replace('/login', true);
        return;
      }

      const user = await fetchUser(params.userId);
      present({ user, tab: query.tab });
    }
  ),
};

stack.addScreen('/users/:userId', UserDetails);

If you never call present(), the screen is not pushed/replaced.

Appearance

Pass NavigationAppearance to <Navigation> to customize styling globally:

import { Navigation, type NavigationAppearance } from '@sigmela/router';

const appearance: NavigationAppearance = {
  tabBar: {
    backgroundColor: '#ffffff',
    iconColor: '#999999',
    iconColorActive: '#007AFF',
    badgeBackgroundColor: '#FF3B30',
    iOSShadowColor: '#00000020',
    title: {
      fontFamily: 'Inter',
      fontSize: 10,
      color: '#999999',
      activeColor: '#007AFF',
      activeFontSize: 12,       // Android: active tab title font size
    },

    // Android-specific
    androidActiveIndicatorEnabled: true,
    androidActiveIndicatorColor: '#007AFF20',
    androidRippleColor: '#007AFF10',
    labelVisibilityMode: 'labeled',

    // Tab bar behavior
    hidden: false,                           // hide/show the tab bar
    tintColor: '#007AFF',                    // iOS: selected tab tint + glow color
    controllerMode: 'automatic',             // iOS 18+: 'automatic' | 'tabBar' | 'tabSidebar'
    minimizeBehavior: 'automatic',           // iOS 26+: 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp'
    nativeContainerBackgroundColor: '#fff',   // native container background
    iOSBlurEffect: 'systemDefault',          // iOS: tab bar blur effect
  },
  header: { /* ScreenStackHeaderConfigProps */ },
  sheet: {
    cornerRadius: 16,
    backgroundColor: '#ffffff',
  },
};

export default function App() {
  return <Navigation router={router} appearance={appearance} />;
}

Hooks

useRouter()

Access the router instance.

useCurrentRoute()

Subscribes to router.getActiveRoute().

Returns ActiveRoute | null (shape from src/types.ts):

type ActiveRoute = {
  routeId: string;
  stackId?: string;
  tabIndex?: number;
  path?: string;
  params?: Record<string, unknown>;
  query?: Record<string, unknown>;
} | null;

useParams<T>() / useQueryParams<T>()

Returns params/query for the current screen (from route context).

useRoute()

Returns route-local context for the current screen:

type RouteLocalContextValue = {
  presentation: StackPresentationTypes;
  params?: Record<string, unknown>;
  query?: Record<string, unknown>;
  pattern?: string;
  path?: string;
};

useTabBar()

Returns the nearest TabBar from context (only inside tab screens).

import { useTabBar } from '@sigmela/router';

function ScreenInsideTabs() {
  const tabBar = useTabBar();

  return (
    <Button
      title="Go to second tab"
      onPress={() => tabBar.onIndexChange(1)}
    />
  );
}

useTabBarHeight()

Returns the tab bar height constant (57). Useful for bottom padding.

useDrawer()

Returns the nearest Drawer from context (only inside drawer screens).

import { useDrawer } from '@sigmela/router';

function ScreenInsideDrawer() {
  const drawer = useDrawer();
  return <Button title="Open menu" onPress={() => drawer.open()} />;
}

useSplitView()

Returns the nearest SplitView from context (only inside split view screens).

Web integration

History API syncing

On web, Router integrates with the browser History API using custom events:

  • router.navigate('/x') writes history.pushState({ __srPath: ... })
  • router.replace('/x') writes history.replaceState({ __srPath: ... })
  • Browser back/forward triggers popstate and Router updates its state accordingly

Important behavioral detail:

  • router.goBack() does not call history.back(). It pops Router state and updates the URL via replaceState (so it doesn’t grow/rewind the browser stack).

syncWithUrl: false

If a route has screenOptions.syncWithUrl = false, Router stores the “real” router path in history.state.__srPath while keeping the visible URL unchanged.

License

MIT