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

@capgo/capacitor-native-navigation

v8.16.3

Published

Capacitor plugin for native navbar, tabbar, safe-area handling, and WebView snapshot transitions.

Readme

@capgo/capacitor-native-navigation

🤖 Building with an AI coding agent? Install Capgo skills so Claude Code, Codex, Cursor, Copilot, or Gemini CLI know how to install and use @capgo/capacitor-native-navigation in your Capacitor app: npx skills add Cap-go/capgo-skills. Then ask your agent: "Add @capgo/capacitor-native-navigation to my Capacitor app and wire it up." Agent setup guides · Hosted MCP

Native navbar, tab bar, safe-area handling, and WebView snapshot transitions for Capacitor apps. Your web framework keeps routing and page rendering while this plugin owns the platform surfaces: a search field under the large title, a tab bar aligned to the start, center, or end, and a trailing sidebar rail.

iOS 27 and iPhone Duo. On iPhone Duo, iOS moves the status bar and the system tab bar into one vertical rail on the folded outer display and on the open display in landscape. Open portrait keeps the horizontal bars. Navbar icon buttons join that rail, and the title stays horizontal. A back or close icon sits at the top. Search can be a detached button, prominent actions stay pinned, and buttons that share a group collapse into one control that expands. The curved center bar stays on the bottom. configure({ verticalBarBehavior: 'disabled' }) keeps the horizontal bars for a screen that should not use the rail.

Built and maintained with AI

We build, improve and fix our plugins with heavy use of AI, so issues usually get fixed in under a day and pull requests get feedback the same day. A human still reviews, tests and merges 50+ pull requests daily.

  • Most plugin issues get a fix pull request the same day, often within a couple of hours, and are usually resolved in one to two days.
  • Every pull request goes through our AI review system, usually within minutes.
  • Our AI pre-checks tests, code review, conventions and security, then a human reviews, tests and merges.
  • A human, Martin, reviews, tests and merges 50+ pull requests every day.
  • In the last 90 days, 95% of plugin issues were resolved.

Learn more about how we build with AI

Demo

| App | Tap flow | SVG icons | Styling | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | | |

| iOS Liquid Glass | Android blur | Android tap flow | Curved tabbar | | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | | |

The Android captures come from Mimesis on Android 16: a large navbar title that collapses on scroll, a translucent native tabbar and a transparent navbar over CSS backdrop-filter blur.

iPhone Duo

Folded, and open in landscape, the system tab bar becomes a vertical rail. Open portrait stays a horizontal bar. Split View puts each window's rail on the outer edge. The rail stays on the camera's physical edge, so the semantic edge is trailing in landscape and can be leading or trailing in Split View and in right-to-left layout. The curved center bar stays on the bottom, and its cradle stays a circle around the button when the window widens.

Icon buttons from the navbar join that rail. A text title stays horizontal, so a back control needs display: 'icon' (or role: 'close') before it sits at the top of the rail. dock: 'detached' draws a button on its own. dock: 'prominent' pins it past overflow and search. axis: 'vertical' puts a text button such as Done on the rail. Items that share group collapse to the first item, and the system expands the rest. search.placement: 'button' is a detached search button on the iPhone Duo rail in iOS 26 and later. A horizontal navbar, Android, and web keep the stacked field. verticalBarCompression: 'preferItems' keeps navbar buttons when the rail runs out of room. Segment rows stay horizontal and clear the rail. Curve and other custom capsules stay on the bottom, and the center button stays a circle. configure({ verticalBarBehavior: 'disabled' }) opts that screen out and keeps the top and bottom bars. Omitting verticalBarBehavior later keeps the current choice. safeAreaChanged reports verticalBarEdge, fold reservedRegions, and the latest hinge sample. verticalBarChanged and hingeChanged fire when those change.

await NativeNavigation.configure({ verticalBarBehavior: 'automatic' });

await NativeNavigation.setNavbar({
  title: 'Notes',
  verticalBarCompression: 'preferItems',
  backButton: { visible: true, title: 'Back', display: 'icon' },
  rightItems: [
    { id: 'done', title: 'Done', dock: 'prominent', axis: 'vertical' },
    { id: 'reply', title: 'Reply', icon: { ios: { sfSymbol: 'arrowshape.turn.up.left' } }, group: 'message' },
    { id: 'forward', title: 'Forward', icon: { ios: { sfSymbol: 'arrowshape.turn.up.right' } }, group: 'message' },
  ],
  search: { placement: 'button', placeholder: 'Search' },
});
.page {
  padding-inline-start: var(--cap-native-navigation-left);
  padding-inline-end: var(--cap-native-navigation-right);
}

--cap-native-navigation-division-x and --cap-native-navigation-division-width mark the fold so a web layout can split around it. The plugin still uses one WebView. On a phone with no rail those inset variables are 0px.

Captured on the iPhone Duo simulator in iOS 27.1.

| Folded, center | Open, center | Open, system | Folded, system | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | | |

Open landscape with the navbar controls in the rail. The back icon sits under the status items, Done is a prominent text button, reply and forward share one group, and search is a detached button. The system tabs stay at the bottom of the rail.

Navbar segment rows

Multi-level native segment rows under the navbar: a scope switcher on top, section chips below it. See Navbar segment rows.

| iOS | Android | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | |

Pull to refresh

Opt-in native pull-to-refresh, shown below the navbar. It works whether the page scrolls window or an inner root such as #app.

| iOS | Android | | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | | |

await NativeNavigation.setPullToRefresh({ enabled: true });
NativeNavigation.addListener('pullToRefresh', async () => {
  await reloadData();
  await NativeNavigation.endPullToRefresh();
});

Features

  • Drive native top navigation and bottom tabs from JavaScript state.
  • Show a native search field under the navbar with setNavbar({ search }). It collapses with a large title and reports navbarSearch.
  • Align a floating tab bar to the start, center, or end, and place the same tabs in a trailing sidebar rail.
  • Use system-owned iOS navigation bars, tab bars, tab gestures, and Liquid Glass rendering.
  • Emit native intent events such as navbarBack, navbarItemTap, navbarSearch, and tabSelect.
  • Enable an optional Android Liquid Glass-style translucent surface for native bars (pair it with a CSS backdrop-filter for blur).
  • Capture WebView snapshots for native-feeling push, back, root, tab, and zoom transitions.
  • Configure tab labels, selected icons, badges, indicators, ripples, tint colors, and dynamic colors.
  • Write CSS inset variables so web content can scroll behind native bars without being hidden.
  • On iPhone Duo, follow the system vertical rail: navbar items join it, safe areas clear it, and a screen can opt out.
  • Stack multi-level native segment rows under the navbar (scope switcher plus section tabs) with setNavbar({ segments }).
  • Opt into native pull-to-refresh (system spinner and haptic on iOS, SwipeRefreshLayout on Android) with setPullToRefresh().
  • Hide the navbar and tabbar while a web modal, sheet, or popover is open. An animated tabbar hide keeps the iOS tab controller and WebView in place.
  • Work with React, Vue, Angular, Svelte, Solid, vanilla JS, and any router that exposes imperative navigation.

Choose a surface

| Surface | Use it for | | --- | --- | | Navbar search | Filter the page under a large title, as in Settings or a library. | | Segment rows | Switch scope and section on the current screen. | | Floating bar, center | The default phone tab bar. | | Floating bar, start or end | A short bar sitting toward one side. | | Sidebar | The same tabs as a trailing rail on iPad, iPhone Duo, and Android. | | Curve | A bottom bar with a raised center action. It stays on the bottom. |

How It Fits

  • Your router still owns route state and page rendering.
  • The app still uses one full-screen WebView.
  • Icons must be serializable descriptors such as SVG strings, SF Symbols, or native resource names.

Modals, sheets, and popovers

Native bars are drawn above the WebView. A modal, sheet, dialog, or popover from the page (ion-modal, ion-popover, <dialog>, a menu) renders inside that WebView, so it stays underneath the navbar and the tabbar.

Hide each bar the overlay would meet, then send the same options again when the overlay closes. Count open overlays when they can stack. The inner dismiss has to leave the bars hidden while an outer overlay is still up.

let overlayCount = 0;

let navbar = {
  title: 'Home',
  backButton: { visible: false },
};

let tabbar = {
  selectedId: 'home',
  tabs: [
    { id: 'home', title: 'Home' },
    { id: 'settings', title: 'Settings' },
  ],
};

async function suspendNativeChrome() {
  overlayCount += 1;
  if (overlayCount > 1) {
    return;
  }

  await NativeNavigation.setNavbar({ ...navbar, hidden: true });
  await NativeNavigation.setTabbar({ ...tabbar, hidden: true, animated: true });
}

async function resumeNativeChrome() {
  overlayCount = Math.max(0, overlayCount - 1);
  if (overlayCount > 0) {
    return;
  }

  await NativeNavigation.setNavbar({ ...navbar, hidden: false });
  await NativeNavigation.setTabbar({ ...tabbar, hidden: false, animated: true });
}

Keep navbar and tabbar as the options for the current route, and update them in the same place you already call setNavbar and setTabbar. Call suspendNativeChrome() before the overlay presents (ionModalWillPresent, ionPopoverWillPresent, or the line before dialog.showModal()). Call resumeNativeChrome() after it is gone (ionModalDidDismiss, ionPopoverDidDismiss, or the dialog close event).

setTabbar({ hidden: false }) without tabs clears the bar. Spread the last options you rendered and change hidden.

Pass animated: true on that tabbar toggle:

  • iOS 26+ system Liquid Glass, when shape is not curve and minimizeBehavior is not onScrollDown. The plugin calls UITabBarController.setTabBarHidden(_:animated:). The tab controller stays mounted and the WebView stays hosted in it. UIKit fades the bar. animated: false on this bar tears the controller down and moves the WebView back to the bridge, which runs during the modal's own animation.
  • Earlier iOS, the curved bar, and the minimize-on-scroll capsule. The bar fades over 200ms and stays mounted.
  • Android. The bar and its backdrop slide and fade over 200ms, then leave the layout. Showing them plays that in reverse. A later setTabbar on a bar that is already visible does not replay the slide-in.

The navbar hide is immediate on iOS and Android. animated on setNavbar applies to title and button changes while the bar is visible.

Insets update when the call resolves, while the tabbar animation is still running:

  • A hidden navbar sets --cap-native-navigation-top and --cap-native-navbar-height to 0. Scroll-linked navbar changes wait until the next setNavbar that shows the bar.
  • A hidden Android tabbar sets --cap-native-navigation-bottom and --cap-native-tabbar-height to 0, so the overlay can use the space the bar is leaving.
  • On the iOS 26+ system tab bar, --cap-native-tabbar-height is already 0: the WebView is hosted inside the tab and does not extend under the bar. An animated hide keeps --cap-native-system-tabbar-height at the nominal bar height (bar plus home-indicator padding). A non-animated hide drops that value as well, because the tab controller is gone.

A popover that stays clear of the bars can leave them up. Hide the navbar for a menu under the title, and the tabbar for a sheet that covers the bottom of the screen.

configure({ enabled: false }) turns the chrome host off. Use hidden for an overlay. On the web there is no native bar; the same calls still update the CSS inset variables.

Keep the modal in this WebView. The overlay is page state, and the router stays in charge of it.

Compatibility

@capgo/capacitor-native-navigation targets Capacitor 8 and Node.js 22+. iOS 27 is supported, including iPhone Duo.

Install

You can use our AI-Assisted Setup to install the plugin. Add the Capgo skills to your AI tool using the following command:

npx skills add https://github.com/cap-go/capacitor-skills --skill capacitor-plugins

Then use the following prompt:

Use the `capacitor-plugins` skill from `cap-go/capacitor-skills` to install the `@capgo/capacitor-native-navigation` plugin in my project.

If you prefer Manual Setup, install the plugin by running the following commands and follow the platform-specific instructions below:

npm install @capgo/capacitor-native-navigation
npx cap sync

Minimal Usage

import { NativeNavigation } from '@capgo/capacitor-native-navigation';

await NativeNavigation.configure({
  contentInsetMode: 'css',
  animationDuration: 360,
});

await NativeNavigation.setNavbar({
  title: 'Home',
  subtitle: 'Native chrome',
  transparent: true,
  scrollBehavior: 'both',
  scrollThreshold: 8,
  backButton: { visible: false },
  rightItems: [
    {
      id: 'compose',
      title: 'Compose',
      icon: {
        svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>',
      },
    },
  ],
});

await NativeNavigation.setTabbar({
  selectedId: 'home',
  labelVisibilityMode: 'labeled',
  icons: true,
  colors: {
    dynamic: true,
  },
  tabs: [
    {
      id: 'home',
      title: 'Home',
      icon: {
        svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 10v10h14V10"/></svg>',
      },
    },
    {
      id: 'settings',
      title: 'Settings',
      icon: {
        svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>',
      },
    },
  ],
});

await NativeNavigation.addListener('tabSelect', ({ id }) => {
  router.navigate(`/${id}`);
});

Native Tab Styling

await NativeNavigation.setTabbar({
  selectedId: 'home',
  labelVisibilityMode: 'selected',
  indicatorColor: '#0A84FF',
  rippleColor: '#330A84FF',
  badgeBackgroundColor: '#FF3B30',
  badgeTextColor: '#FFFFFF',
  colors: {
    dynamic: true,
    tint: '#0A84FF',
    inactiveTint: '#8E8E93',
  },
  tabs: [
    {
      id: 'home',
      title: 'Home',
      icon: { ios: { sfSymbol: 'house' }, android: { resource: 'ic_home' } },
      selectedIcon: { ios: { sfSymbol: 'house.fill' }, android: { resource: 'ic_home_filled' } },
    },
  ],
});

Android Liquid Glass

On Android 12+, floating tabbars use liquidGlass automatically when setTabbar omits glass (pass glass: { effect: 'none' } for a flat fill). Android draws a translucent tint without live blur: rendering the WebView from native code to blur it flashes the page or swallows flings. For blur, place an element with backdrop-filter: blur(...) in your page under the bar (the floating tabbar is 64px tall, 24px from the sides, at most 430px wide, 10px above the system navigation bar). Tune colors.background alpha and glass.surfaceAlpha (default 0.62) together. When the system font size is increased, the tab bar grows with the label line height and reported tabbarHeight / bottom insets stay aligned with the native bar.

await NativeNavigation.configure({
  glass: {
    effect: 'liquidGlass',
    blurRadius: 18,
    surfaceAlpha: 0.62,
  },
});

await NativeNavigation.setTabbar({
  selectedId: 'home',
  tabs,
  colors: {
    background: '#F8FFFFFF',
  },
});

Transition Flow

const transition = await NativeNavigation.beginTransition({ direction: 'forward' });

router.navigate('/detail');
await router.ready?.();

await NativeNavigation.setNavbar({
  title: 'Detail',
  backButton: { visible: true, title: 'Back' },
});

await NativeNavigation.finishTransition({
  id: transition.id,
  direction: 'forward',
});

Use With @capgo/capacitor-transitions

Use @capgo/capacitor-native-navigation for the native navbar, tabbar, safe-area insets, and native intent events. Use @capgo/capacitor-transitions for the WebView page stack underneath that native chrome.

npm install @capgo/capacitor-native-navigation @capgo/capacitor-transitions
npx cap sync

Initialize both packages once when the app starts:

import { NativeNavigation } from '@capgo/capacitor-native-navigation';
import '@capgo/capacitor-transitions';
import { initTransitions, setupRouterOutlet, setDirection } from '@capgo/capacitor-transitions/react';

initTransitions({ platform: 'auto' });

const outlet = document.querySelector('cap-router-outlet');
if (outlet) {
  setupRouterOutlet(outlet, { platform: 'auto', swipeGesture: 'auto' });
}

await NativeNavigation.configure({
  contentInsetMode: 'css',
});

Keep the transition outlet focused on pages. Do not render a web header or footer when native chrome owns those surfaces:

<cap-router-outlet platform="auto" swipe-gesture="auto">
  <cap-page>
    <cap-content slot="content" fullscreen>
      <main class="page">Inbox content</main>
    </cap-content>
  </cap-page>
</cap-router-outlet>
.page {
  min-height: 100dvh;
  padding-top: var(--cap-native-navigation-top);
  padding-bottom: var(--cap-native-navigation-bottom);
}

Drive both packages from the same router actions:

async function openMessage(id: string) {
  setDirection('forward');
  await router.push(`/messages/${id}`);
  await NativeNavigation.setNavbar({
    title: 'Message',
    backButton: { visible: true, title: 'Inbox' },
  });
}

await NativeNavigation.addListener('navbarBack', () => {
  setDirection('back');
  router.back();
});

await NativeNavigation.addListener('tabSelect', ({ id }) => {
  setDirection('root');
  router.push(`/${id}`);
});

Pick one animation layer per navigation. For normal route pushes, let @capgo/capacitor-transitions animate the WebView pages and update native bars with setNavbar / setTabbar. For shared-element or zoom routes, use beginZoomTransition / finishZoomTransition from this plugin and skip the web page transition for that navigation.

Scroll-driven navbar

Opt in per screen with setNavbar({ scrollBehavior: 'both' }). Defaults stay none, so existing apps keep static chrome. Native iOS and Android listen to scroll on window, #app, and any element marked with data-cap-native-navigation-scroll. Use reportNavbarScroll() only for custom scroll roots.

The example app route Scroll header demo (home tile) shows a long list with scrollBehavior: 'both'. Scroll down to hide the bar. Scroll up to bring it back before the list reaches the top.

| At rest | Hide, then reveal | | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | |

Navbar search

Pass search to setNavbar to draw a native field under the bar and above any segment rows. The row is 52pt on iOS and 52dp on Android, and that height is included in the top inset. iOS uses a UISearchBar. Android uses a text field. The web preview draws the same field above the page.

hidesWhenCollapsed defaults to true when the navbar is a large title. Once that title collapses, the row height becomes 0 and the top inset shrinks with it. On the web preview the field hides with the navbar when scroll behavior collapses the bar. Large-title collapse itself is native.

import { NativeNavigation } from '@capgo/capacitor-native-navigation';

let query = '';

const renderNavbar = () =>
  NativeNavigation.setNavbar({
    title: 'Settings',
    large: true,
    search: {
      placeholder: 'Search',
      text: query,
      hidesWhenCollapsed: true,
    },
  });

await NativeNavigation.addListener('navbarSearch', async ({ text, reason }) => {
  query = reason === 'cancel' ? '' : text;
  await renderNavbar();
});

await renderNavbar();

Send the latest text on the next setNavbar so the field stays in sync. While the field is focused, an unchanged text is left alone. On iOS and Android, omitting search removes the field. The web implementation keeps the previous search object when the call omits it, so pass search: { hidden: true } to clear it there too.

The example Settings tab uses this field to filter an inset grouped list, and its appearance control switches the demo between light, dark, and the system scheme. The Navbar playground has a Search field toggle.

iPhone 17 simulator, iOS 27.0.

Tab bar placement and alignment

style.alignment places a floating bar inside the horizontal margin: start, center (the default), or end. start and end follow layoutDirection. Curve bars and sidebar rails ignore alignment.

style.placement chooses the chrome:

  • auto lets iOS 18 and newer decide. Compact iPhone stays on the bottom. iPad and iPhone Duo can use the trailing sidebar.
  • bottom keeps a bottom bar.
  • sidebar asks for the trailing rail. On iOS 18 and newer that is the system tab bar in sidebar mode, including on iOS 18 through 25. iPad portrait shows that mode as a top tab bar with the sidebar button. A compact iPhone may stay on the bottom when UIKit does not offer a sidebar. Android draws an 84dp rail, inset 12dp from the trailing edge, starting below the navbar. The navbar stays full width. The web preview reports the same left or right inset.

curve always stays on the bottom. sidebar ignores minimizeBehavior and drag-to-select.

await NativeNavigation.setTabbar({
  tabs,
  selectedId: 'home',
  style: {
    shape: 'floating',
    alignment: 'end',
    placement: 'bottom',
  },
});

await NativeNavigation.setTabbar({
  tabs,
  selectedId: 'home',
  style: {
    shape: 'floating',
    placement: 'sidebar',
  },
});

The example Tabbar playground and the Settings tab both expose alignment and placement. Pad page content with --cap-native-navigation-left and --cap-native-navigation-right so a rail does not cover it.

iPad Pro 11-inch simulator, iOS 27.0. Alignment moves the floating capsule; the iOS 26 system tab bar does not. Sidebar placement uses UIKit's top tab bar and sidebar button.

Navbar segment rows (multi-level top bar)

Pass segments to setNavbar to stack rows of native segments under the bar, such as a scope switcher on top and section tabs below it. Rows draw top to bottom, each one adds 44pt (iOS) or 48dp (Android) to the navbar and to the reported insets. iOS uses UISegmentedControl / glass pills, Android uses Material segmented buttons and filter chips.

import { NativeNavigation } from '@capgo/capacitor-native-navigation';

const sections = {
  account: ['General', 'Notifications', 'Password', 'Two-factor'],
  organization: ['General', 'Members', 'Plans', 'Usage', 'Credits', 'Billing', 'Security', 'Webhooks'],
};
const selection = { scope: 'account', section: 'general' };

const renderNavbar = () =>
  NativeNavigation.setNavbar({
    title: 'Settings',
    segments: [
      {
        id: 'scope',
        selectedId: selection.scope,
        items: [
          { id: 'account', title: 'Account' },
          { id: 'organization', title: 'Organization' },
        ],
      },
      {
        id: 'section',
        style: 'scroll', // horizontally scrolling chips; `auto` picks `segmented` for up to 4 items
        selectedId: selection.section,
        items: sections[selection.scope].map((title) => ({ id: title.toLowerCase(), title })),
      },
    ],
  });

await NativeNavigation.addListener('navbarSegmentSelect', async ({ groupId, id }) => {
  if (groupId === 'scope') {
    selection.scope = id;
    selection.section = 'general'; // the second level depends on the first one
  } else {
    selection.section = id;
  }
  await renderNavbar();
});

await renderNavbar();

Selection moves natively when the user taps, but every setNavbar call applies selectedId again. Keep the selected ids in your state and pass them on every later setNavbar call; to reject a selection, send the previous selectedId. Items accept icon, accessibilityLabel and enabled like navbar buttons. Pass segments: [] to remove the rows.

The example app Navbar playground has a Segment rows toggle with this exact setup.

Zoom Transition

import { beginZoomTransition, finishZoomTransition } from '@capgo/capacitor-native-navigation';

const card = document.querySelector('[data-photo-card]');
if (card) {
  const transition = await beginZoomTransition(card, { cornerRadius: 18 });

  router.navigate('/photo/42');
  await router.ready?.();

  await finishZoomTransition(undefined, {
    id: transition.id,
    cornerRadius: 18,
  });
}

CSS Insets

With contentInsetMode: 'css', the plugin updates these variables on document.documentElement:

.app-scroll {
  height: 100dvh;
  overflow: auto;
  padding-top: calc(var(--cap-native-navigation-top) + 24px);
  padding-left: var(--cap-native-navigation-left);
  padding-right: var(--cap-native-navigation-right);
  scroll-padding-bottom: calc(var(--cap-native-navigation-bottom) + 24px);
}

.page {
  min-height: 100dvh;
  padding-bottom: calc(var(--cap-native-navigation-bottom) + 24px);
}

Available variables:

  • --cap-native-navigation-top
  • --cap-native-navigation-right
  • --cap-native-navigation-bottom
  • --cap-native-navigation-left
  • --cap-native-navbar-height
  • --cap-native-tabbar-height
  • --cap-native-system-tabbar-height (nominal tab bar height; use on iOS when --cap-native-tabbar-height is 0 for hosted system bars). On an iPhone Duo rail this is the rail thickness, and the overlap is --cap-native-navigation-left or --cap-native-navigation-right.
  • --cap-native-navigation-vertical-bar-edge (leading, trailing, or empty)
  • --cap-native-navigation-division-x and --cap-native-navigation-division-width (the fold, or 0px)

Web Components

The package can register optional custom elements for framework-agnostic declarative setup:

import { defineNativeNavigationElements } from '@capgo/capacitor-native-navigation';

defineNativeNavigationElements();
<cap-native-navigation-provider enabled="true" content-inset-mode="css"></cap-native-navigation-provider>

<cap-native-navbar
  title="Settings"
  large
  search='{"placeholder":"Search","hidesWhenCollapsed":true}'
></cap-native-navbar>

<cap-native-tabbar
  selected-id="home"
  tabs='[{"id":"home","title":"Home","icon":{"ios":{"sfSymbol":"house.fill"}}}]'
  tabbar-style='{"shape":"floating","alignment":"center","placement":"auto"}'
></cap-native-tabbar>

Icon Descriptors

const icon = {
  svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 10.5 12 3l9 7.5"/></svg>',
  width: 24,
  height: 24,
  template: true,
  src: 'shared_asset_name',
  ios: {
    svg: '<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/></svg>',
    sfSymbol: 'house.fill',
    image: 'BundledAssetName',
  },
  android: {
    svg: '<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/></svg>',
    resource: 'ic_menu_view',
    image: 'bundled_drawable_name',
  },
};

Inline SVG supports the icon-focused subset used by common sets such as Lucide and Feather: path, line, polyline, polygon, circle, and rect. The SVG is rendered as a template image by default, so native tint colors can recolor it without bundling a platform asset.

Platform Notes

  • iOS uses UIKit UINavigationBar, UISearchBar, UITabBar, and UITabBarController so the system owns tab interaction, Liquid Glass rendering, sidebar placement on iOS 18+, and safe-area behavior.
  • Android uses an AppCompat Toolbar, a text field under that toolbar, and a native tab surface. Sidebar placement is a trailing rail. The navbar stays full width.
  • Web mirrors inset variables, the search field, sidebar insets, floating-bar alignment, and events for local development.

Example App

The example-app/ folder is a vanilla JS Capacitor demo linked with file:...

cd example-app
npm install
npm run build
npx cap add ios
npx cap add android
npx cap sync

API

Framework-agnostic native navigation chrome API.

configure(...)

configure(options?: NativeNavigationConfigureOptions | undefined) => Promise<NativeNavigationInsetsResult>

Configure the native chrome host and content inset behavior.

| Param | Type | | ------------- | --------------------------------------------------------------------------------------------- | | options | NativeNavigationConfigureOptions |

Returns: Promise<NativeNavigationInsetsResult>


setNavbar(...)

setNavbar(options: NativeNavigationNavbarOptions) => Promise<NativeNavigationInsetsResult>

Render or update the native navbar.

| Param | Type | | ------------- | --------------------------------------------------------------------------------------- | | options | NativeNavigationNavbarOptions |

Returns: Promise<NativeNavigationInsetsResult>


reportNavbarScroll(...)

reportNavbarScroll(options: NativeNavigationNavbarScrollEvent) => Promise<void>

Report scroll samples for scrollBehavior. Use only for scroll roots that are not already wired by setNavbar (window, #app, or [data-cap-native-navigation-scroll]). iOS, Android, and the web fallback install those listeners automatically when scrolling is enabled.

| Param | Type | | ------------- | ----------------------------------------------------------------------------------------------- | | options | NativeNavigationNavbarScrollEvent |


setPullToRefresh(...)

setPullToRefresh(options: NativeNavigationPullToRefreshOptions) => Promise<void>

Enable or disable the native pull-to-refresh gesture. Listen for pullToRefresh, reload your data, then call endPullToRefresh() to hide the spinner. No-op on web.

| Param | Type | | ------------- | ----------------------------------------------------------------------------------------------------- | | options | NativeNavigationPullToRefreshOptions |

Since: 8.11.0


endPullToRefresh()

endPullToRefresh() => Promise<void>

Stop the native pull-to-refresh spinner once the refresh is done.

Since: 8.11.0


setTabbar(...)

setTabbar(options: NativeNavigationTabbarOptions) => Promise<NativeNavigationInsetsResult>

Render or update the native tabbar.

| Param | Type | | ------------- | --------------------------------------------------------------------------------------- | | options | NativeNavigationTabbarOptions |

Returns: Promise<NativeNavigationInsetsResult>


beginTransition(...)

beginTransition(options?: NativeNavigationBeginTransitionOptions | undefined) => Promise<NativeNavigationTransitionResult>

Capture the current WebView and prepare a native transition.

| Param | Type | | ------------- | --------------------------------------------------------------------------------------------------------- | | options | NativeNavigationBeginTransitionOptions |

Returns: Promise<NativeNavigationTransitionResult>


finishTransition(...)

finishTransition(options?: NativeNavigationFinishTransitionOptions | undefined) => Promise<NativeNavigationTransitionResult>

Animate from the captured WebView snapshot to the current live WebView.

| Param | Type | | ------------- | ----------------------------------------------------------------------------------------------------------- | | options | NativeNavigationFinishTransitionOptions |

Returns: Promise<NativeNavigationTransitionResult>


getPluginVersion()

getPluginVersion() => Promise<PluginVersionResult>

Returns the platform implementation version marker.

Returns: Promise<PluginVersionResult>


addListener('navbarBack', ...)

addListener(eventName: 'navbarBack', listenerFunc: (event: NativeNavigationBackEvent) => void) => Promise<PluginListenerHandle>

| Param | Type | | ------------------ | --------------------------------------------------------------------------------------------------- | | eventName | 'navbarBack' | | listenerFunc | (event: NativeNavigationBackEvent) => void |

Returns: Promise<PluginListenerHandle>


addListener('navbarItemTap', ...)

addListener(eventName: 'navbarItemTap', listenerFunc: (event: NativeNavigationBarItemTapEvent) => void) => Promise<PluginListenerHandle>

| Param | Type | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | eventName | 'navbarItemTap' | | listenerFunc | (event: NativeNavigationBarItemTapEvent) => void |

Returns: Promise<PluginListenerHandle>


addListener('navbarSearch', ...)

addListener(eventName: 'navbarSearch', listenerFunc: (event: NativeNavigationNavbarSearchEvent) => void) => Promise<PluginListenerHandle>

Fired when the navbar search field changes.

| Param | Type | | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | eventName | 'navbarSearch' | | listenerFunc | (event: NativeNavigationNavbarSearchEvent) => void |

Returns: Promise<PluginListenerHandle>

Since: 8.14.0


addListener('navbarSegmentSelect', ...)

addListener(eventName: 'navbarSegmentSelect', listenerFunc: (event: NativeNavigationNavbarSegmentSelectEvent) => void) => Promise<PluginListenerHandle>

Fired when the user selects an item in a navbar segment row.

| Param | Type | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | | eventName | 'navbarSegmentSelect' | | listenerFunc | (event: NativeNavigationNavbarSegmentSelectEvent) => void |

Returns: Promise<PluginListenerHandle>

Since: 8.13.0


addListener('tabSelect', ...)

addListener(eventName: 'tabSelect', listenerFunc: (event: NativeNavigationTabSelectEvent) => void) => Promise<PluginListenerHandle>

| Param | Type | | ------------------ | ------------------------------------------------------------------------------------------------------------- | | eventName | 'tabSelect' | | listenerFunc | (event: NativeNavigationTabSelectEvent) => void |

Returns: Promise<PluginListenerHandle>


addListener('safeAreaChanged', ...)

addListener(eventName: 'safeAreaChanged', listenerFunc: (event: NativeNavigationSafeAreaChangedEvent) => void) => Promise<PluginListenerHandle>

| Param | Type | | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | eventName | 'safeAreaChanged' | | listenerFunc | (event: NativeNavigationSafeAreaChangedEvent) => void |

Returns: Promise<PluginListenerHandle>


addListener('verticalBarChanged', ...)

addListener(eventName: 'verticalBarChanged', listenerFunc: (event: NativeNavigationVerticalBarChangedEvent) => void) => Promise<PluginListenerHandle>

Fired when the iPhone Duo vertical rail appears, disappears, or changes edge. safeAreaChanged carries the same edge on every inset update.

| Param | Type | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | eventName | 'verticalBarChanged' | | listenerFunc | (event: NativeNavigationVerticalBarChangedEvent) => void |

Returns: Promise<PluginListenerHandle>

Since: 8.14.0


addListener('hingeChanged', ...)

addListener(eventName: 'hingeChanged', listenerFunc: (event: NativeNavigationHingeChangedEvent) => void) => Promise<PluginListenerHandle>

Fired when the iPhone Duo hinge changes, or when hinge updates stop. Prefer hinge.status over hinge.angle when a coarse pose is enough.

| Param | Type | | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | eventName | 'hingeChanged' | | listenerFunc | (event: NativeNavigationHingeChangedEvent) => void |

Returns: Promise<PluginListenerHandle>

Since: 8.14.0


addListener('transitionStart', ...)

addListener(eventName: 'transitionStart', listenerFunc: (event: NativeNavigationTransitionEvent) => void) => Promise<PluginListenerHandle>

| Param | Type | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | eventName | 'transitionStart' | | listenerFunc | (event: NativeNavigationTransitionEvent) => void |

Returns: Promise<PluginListenerHandle>


addListener('transitionEnd', ...)

addListener(eventName: 'transitionEnd', listenerFunc: (event: NativeNavigationTransitionEvent) => void) => Promise<PluginListenerHandle>

| Param | Type | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | eventName | 'transitionEnd' | | listenerFunc | (event: NativeNavigationTransitionEvent) => void |

Returns: Promise<PluginListenerHandle>


addListener('pullToRefresh', ...)

addListener(eventName: 'pullToRefresh', listenerFunc: () => void) => Promise<PluginListenerHandle>

Fired when the user pulls to refresh. The spinner keeps spinning until endPullToRefresh() is called.

| Param | Type | | ------------------ | ---------------------------- | | eventName | 'pullToRefresh' | | listenerFunc | () => void |

Returns: Promise<PluginListenerHandle>

Since: 8.11.0


Interfaces

NativeNavigationInsetsResult

Returned by methods that may change safe content bounds.

| Prop | Type | | ------------ | ------------------------------------------------------------------------- | | insets | NativeNavigationInsets |

NativeNavigationInsets

Insets exposed to web content.

| Prop | Type | Description | Since | | ------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | | top | number | | | | right | number | | | | bottom | number | | | | left | number | | | | navbarHeight | number | | | | tabbarHeight | number | Bottom overlap inset for web layout when the WebView does not already reserve space for the native tab bar. On iOS 26+ with a system-hosted tab bar this stays 0 so content is not lifted twice. | | | systemTabbarHeight | number | Nominal native tab bar height (bar plus home-indicator padding on iOS) even when tabbarHeight is 0. Stays available while the tab bar is soft-hidden with setTabbar({ hidden: true, animated: true }). | | | verticalBarEdge | NativeNavigationVerticalBarEdge | null | Semantic edge of the vertical rail, or null when the bars are horizontal. On a rail, left or right carries the rail thickness and tabbarHeight stays 0 when the WebView is already beside it. systemTabbarHeight is that thickness rather than a bottom bar. | 8.14.0 | | reservedRegions | NativeNavigationReservedRegion[] | Fold and occlusion rectangles reported by iOS, in the root view's points. Empty when the device has no reserved regions. | 8.14.0 | | hinge | NativeNavigationHinge | null | Latest hinge sample. null when the device has no hinge. Angle updates are delivered on hingeChanged and are not pushed through this snapshot on every tick. | 8.14.0 |

NativeNavigationReservedRegion

A region iOS reserves inside the window, such as the fold or an occlusion.

| Prop | Type | | ------------ | -------------------------------------- | | kind | 'division' | 'occlusion' | | x | number | | y | number | | width | number | | height | number | | active | boolean |

NativeNavigationHinge

iPhone Duo hinge sample. angle is radians.

| Prop | Type | | ------------ | -------------------------------------------------------------------- | | angle | number | | status | 'unknown' | 'closed' | 'partiallyOpen' | 'fullyOpen' |

NativeNavigationConfigureOptions

Global plugin configuration.

| Prop | Type | Description | Since | | ------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | enabled | boolean | Enables or disables the native chrome host. | | | platformStyle | NativeNavigationPlatformStyle | Native style preference. auto uses the current platform. | | | contentInsetMode | NativeNavigationContentInsetMode | When css, the plugin writes CSS variables on document.documentElement. | | | animationDuration | number | Default native transition duration in milliseconds.