
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
NativeTabsnow exposes ascreenListenersprop, 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-routeror can't update right now, the patch approach below still works and follows the same pattern.
Prerequisites
- Expo SDK 55+ with
expo-routerandNativeTabs expo-hapticsinstalled (npx expo install expo-haptics)
Step-by-Step
1. Start the patch
bun patch expo-routerBun 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.js3. 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 functionLight 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
NativeTabsnatively.
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.
Join the newsletter