feat(tabs): iOS Liquid Glass tab bar via NativeTabs - #269
Conversation
Adds an iOS-only override using expo-router/unstable-native-tabs. On iOS 26+ this renders Apple's Liquid Glass tab bar; on iOS 18 and below it falls back to the traditional UITabBar style. Android (and any non-iOS platform) continues to use the existing JS Tabs in _layout.tsx. SF Symbols: book.closed, checklist, sparkles, person.2, newspaper. A hidden 'index' Trigger keeps the cold-start redirect chain (/(tabs)/index → Learn) working. Analytics + haptics are wired via a pathname-watching useEffect since NativeTabs has no tabPress listener. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NativeTabs renders a native UITabBar, so @react-navigation/bottom-tabs' useBottomTabBarHeight throws on iOS with "Couldn't find the bottom tab bar height. Are you inside a screen in Bottom Tab Navigator?". The Checklist and Companion screens both relied on it for layout. New hook hooks/useTabBarHeight.ts dispatches at module load: iOS uses 49pt + bottom safe-area inset (approximating UITabBar); other platforms keep the existing JS hook. Platform.OS is stable per session so the exported reference is stable — no rules-of-hooks violation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NativeTabs (iOS) renders a floating Liquid Glass UITabBar that overlays content rather than carving its own viewport — so the Companion screen's stickyContainer (input + disclaimer) fell behind the bar. JS Tabs on Android already excludes its bar from the viewport, so this is iOS-only. Add paddingBottom: tabBarHeight to the stickyContainer when Platform.OS === 'ios'. The existing keyboard animation already accounts for this offset via tabBarCompensation, so input alignment when typing is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous 49 + insets.bottom value double-counted the home-indicator margin that Liquid Glass already builds in below the floating pill, leaving a visible dead zone above the bar in the Companion screen. Drop the inset; the pill's visual height alone is ~50pt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50pt was undershoot — the Companion disclaimer fell behind the floating bar. 70pt gives the pill enough clearance with a small breathing gap above. Empirically tuned between the previous 83pt (too tall, dead zone) and 50pt (too short, disclaimer hidden). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70pt was still too tight — disclaimer was butted against the Liquid Glass pill. 80pt gives proper breathing room above the bar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multiline TextInput on iOS aligns text to TOP by default, leaving the "Ask anything..." placeholder biased above the pill's vertical center. Adding textAlignVertical: 'center' centers it on Android and improves the visual on iOS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Refresh stale comment in useTabBarHeight (50pt → 80pt empirical) - Skip trackTabSwitch when pathname is on a non-tab route (e.g. /post-details, /account-settings). Without this guard the iOS effect was emitting phantom tab-switch events whenever the user opened an inner screen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (17)
Note
|
Lesson/practice/task/intro pages are immersive single-page experiences with their own Next button at the bottom of the screen. With NativeTabs on iOS the floating Liquid Glass bar overlays that button. Add the flow path patterns to HIDDEN_TAB_BAR_ROUTES so both iOS (via NativeTabs hidden prop) and Android (via tabBarStyle.display='none') hide the bar on these screens. Submodule hubs and the practice/tasks list pages stay visible because their URLs lack the trailing slash in the pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiding the tab bar on Learn immersive flows let lesson/practice/task pages extend to the device bottom, putting their absolute-positioned navigationContainer's Next button inside the home-indicator gesture area. Bump paddingBottom from 15 to 34 across all 5 flow page styles to clear it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PostCommentItem.tsx renders t('home.reply') for the inline Reply button
beneath top-level comments, but that key was never defined — only
home.replyingTo and home.replyPlaceholder existed. Users saw the
literal key string "home.reply" instead of "Reply". Added the key to
all four locales in the home namespace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PostCommentItem.tsx referenced 5 keys that were never defined in any
locale, causing raw key strings ("home.viewReplies", "home.deleteComment",
etc.) to render in place of translated text. Added:
- viewReplies, hideReplies (replies toggle label under top-level comments)
- deleteComment, deleteCommentConfirm, failedDeleteComment (delete flow
alert + error toast)
Each added to en/es/hi/vi with a real per-locale translation, not
English copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two corrections in one pass: - 34pt was still tight per visual review; bump to 50pt for proper breathing room above the home indicator. - Earlier sweep missed 3 files that use 'footer' instead of 'navigationContainer' for the bottom button row: intro, practice pages (matching activity), and quizzes. Bring them in line. All 8 Learn flow page styles now use paddingBottom: 50pt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
NativeTabsfromexpo-router/unstable-native-tabs→ free Liquid Glass UITabBar on iOS 26, traditional UITabBar fallback on iOS 18–. Android stays on the current JS Tabs implementation._layout.ios.tsxas a platform-specific override;_layout.tsxis untouched and serves as both the Android implementation and the universal fallback Expo Router requires.book.closed/checklist/sparkles/person.2/newspaper.Notable design choices
Why iOS-only: real-world reports flag Android NativeTabs rendering bugs (icons not showing consistently). Audience skews iOS, so iOS-only delivers the biggest UX win with zero Android regression risk. When Expo lands the Android fixes we can add
_layout.android.tsx.Why we don't rename
_layout.tsx→_layout.android.tsx: Expo Router requires the unsuffixed fallback file to exist. Renaming crashes the router with"The file ./(tabs)/_layout.ios.tsx does not have a fallback sibling file without a platform extension."Add the.ios.tsxalongside; never rename.Plumbing changes
NativeTabs renders a native UITabBar, which has two implications:
useBottomTabBarHeight()throws — there's no JS navigator to measure. New wrapperhooks/useTabBarHeight.tsdispatches at module load: iOS returns 80pt (empirically tuned to clear the Liquid Glass pill with a small breathing gap); Android calls the original JS hook.Checklist/index.tsxandcompanion/index.tsxswapped over.Platform.OS === 'ios' && { paddingBottom: tabBarHeight }on thestickyContainer.Other porting:
<NativeTabs.Trigger name='index' hidden />so the existing(tabs)/index.tsx<Redirect href='/(tabs)/Learn' />cold-start chain works.HIDDEN_TAB_BAR_ROUTESported via thehiddenprop on<NativeTabs>itself.screenListeners.tabPress, so analytics + haptics fire via a pathname-watchinguseEffect. Skips non-tab paths to avoid phantom tab-switch events when navigating to inner screens like/post-details.textAlignVertical: 'center'added to the Companion input for nicer text positioning.Test plan
tab_switchevent with correct from/to labelsuseTabBarHeight()returns the JS hook value; no iOS-only padding applied🤖 Generated with Claude Code