Adding Haptic Feedback to Expo Router Native Tabs

Adding Haptic Feedback to Expo Router Native Tabs

Beto   -   March 2026

NativeTabs renders truly native tab bars on iOS via UITabBarController, but that means there's no JS-level tabPress listener — so no haptic feedback out of the box. The fix: patch expo-router with bun patch.

Update: The latest version of NativeTabs now exposes a screenListeners prop, so you can add haptics without patching:

<NativeTabs
  screenListeners={{
    tabPress: () => {
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    },
  }}
>
  {/* ...triggers */}
</NativeTabs>

If you're on an older version of expo-router or can't update right now, the patch approach below still works and follows the same pattern.

Prerequisites

  • Expo SDK 55+ with expo-router and NativeTabs
  • expo-haptics installed (npx expo install expo-haptics)

Step-by-Step

1. Start the patch

bun patch expo-router

Bun tells you to edit node_modules/expo-router and commit when done.

2. Open the file to edit

node_modules/expo-router/build/native-tabs/NativeBottomTabsNavigator.js

3. Add the haptics import

At the top, next to the other imports, add:

const Haptics = __importStar(require("expo-haptics"));

The file uses CommonJS with __importStar (TypeScript-generated) — match that style.

4. Add the haptic trigger

Find the onTabChange callback and add the haptics call at the top:

const onTabChange = (0, react_1.useCallback)((tabKey) => {
    if (process.env.EXPO_OS === 'ios') {
        Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    }
    const descriptor = descriptors[tabKey];
    // ...rest of the function

Light impact feels subtle and native for frequent tab taps. The EXPO_OS guard keeps it iOS-only since NativeTabs only renders there.

5. Commit the patch

bun patch --commit 'node_modules/expo-router'

Bun generates the .patch file in patches/ and updates package.json automatically — no manual patchedDependencies editing needed.

6. Verify

Run bun install — you should see the patch apply. Build on a physical iOS device (haptics don't work on simulator) and feel the tap on every tab switch.

The Final Diff

diff --git a/build/native-tabs/NativeBottomTabsNavigator.js b/build/native-tabs/NativeBottomTabsNavigator.js
index 3eab9adbe215bb80c15073e197b075e93b2d9c31..7d002126aee3f505906eb08dc7abe1e0d636bdff 100644
--- a/build/native-tabs/NativeBottomTabsNavigator.js
+++ b/build/native-tabs/NativeBottomTabsNavigator.js
@@ -39,6 +39,7 @@ exports.NativeTabsNavigator = NativeTabsNavigator;
 exports.NativeTabsNavigatorWrapper = NativeTabsNavigatorWrapper;
 const native_1 = require("@react-navigation/native");
 const react_1 = __importStar(require("react"));
+const Haptics = __importStar(require("expo-haptics"));
 const NativeBottomTabsRouter_1 = require("./NativeBottomTabsRouter");
 const NativeTabTrigger_1 = require("./NativeTabTrigger");
 const NativeTabsView_1 = require("./NativeTabsView");
@@ -101,6 +102,9 @@ function NativeTabsNavigator({ children, backBehavior = defaultBackBehavior, lab
     }
     const focusedIndex = visibleFocusedTabIndex >= 0 ? visibleFocusedTabIndex : 0;
     const onTabChange = (0, react_1.useCallback)((tabKey) => {
+        if (process.env.EXPO_OS === 'ios') {
+          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+        }
         const descriptor = descriptors[tabKey];
         const route = descriptor.route;
         navigation.emit({

One import, four lines in the callback. Small patch, big UX win.

Tips

  • Patch the build/ files, not TypeScript source — Metro loads the compiled JS.
  • Re-generate the patch when you upgrade expo-router — line numbers will shift.
  • Check upstream before upgrading — Expo may add haptics to NativeTabs natively.

Want to go deeper?

This patch came straight from Inkigo, a real, shipped app on the App Store. If you're curious how it feels in practice, download it and tap through the tabs.

Pro Members get access to the full Inkigo source code, along with many other premium resources, including all my courses.

If you want to go beyond quick patches and really understand how to build apps that feel native, my React Native course covers advanced Expo Router patterns, platform-specific details, and the kind of polish that separates side projects from shipped products.

I also share practical tips, experiments, and updates in my newsletter if you want to stay in the loop.

Want to partner with me? You can reach me at beto@codewithbeto.dev.

Let's connect!