Skip to content

feat(tabs): iOS Liquid Glass tab bar via NativeTabs - #269

Draft
Savar-G wants to merge 13 commits into
mainfrom
feat/native-tabs-ios
Draft

feat(tabs): iOS Liquid Glass tab bar via NativeTabs#269
Savar-G wants to merge 13 commits into
mainfrom
feat/native-tabs-ios

Conversation

@Savar-G

@Savar-G Savar-G commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • iOS-only adoption of NativeTabs from expo-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.
  • Added _layout.ios.tsx as a platform-specific override; _layout.tsx is untouched and serves as both the Android implementation and the universal fallback Expo Router requires.
  • SF Symbols picked per tab: 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.tsx alongside; never rename.

Plumbing changes

NativeTabs renders a native UITabBar, which has two implications:

  1. useBottomTabBarHeight() throws — there's no JS navigator to measure. New wrapper hooks/useTabBarHeight.ts dispatches 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.tsx and companion/index.tsx swapped over.
  2. Floating bar overlays content — JS Tabs carves a viewport above the bar; NativeTabs floats. Bottom-aligned containers (like Companion's input/disclaimer block) get visually occluded on iOS. Fixed with Platform.OS === 'ios' && { paddingBottom: tabBarHeight } on the stickyContainer.

Other porting:

  • Hidden <NativeTabs.Trigger name='index' hidden /> so the existing (tabs)/index.tsx <Redirect href='/(tabs)/Learn' /> cold-start chain works.
  • HIDDEN_TAB_BAR_ROUTES ported via the hidden prop on <NativeTabs> itself.
  • NativeTabs has no screenListeners.tabPress, so analytics + haptics fire via a pathname-watching useEffect. 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

  • iOS 26 simulator: Liquid Glass tab bar renders, all 5 tabs work
  • iOS 26: cold-start lands on Learn (the hidden index Trigger + Redirect chain)
  • iOS 26: Companion screen — input + disclaimer sit above the floating bar with breathing room (was the main iteration); keyboard slides input correctly above keyboard top
  • iOS 26: Checklist last items not clipped behind the bar
  • iOS 26: tab switch fires haptic and PostHog tab_switch event with correct from/to labels
  • iOS 26: opening a hidden-bar inner route (e.g. PostDetails) hides the bar and does NOT fire a phantom tab_switch event
  • iOS 18 simulator: traditional UITabBar still renders (no Liquid Glass)
  • Android emulator: zero behavior change — _layout.tsx is the file picked; useTabBarHeight() returns the JS hook value; no iOS-only padding applied

🤖 Generated with Claude Code

Savar-G and others added 8 commits May 18, 2026 16:17
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>
@Savar-G
Savar-G requested review from a team as code owners May 19, 2026 01:16
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Savar-G has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 37 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4a4b6ed-a6e0-4fbf-9b2d-d0e235dd6665

📥 Commits

Reviewing files that changed from the base of the PR and between ae6fb93 and 9a7e66a.

📒 Files selected for processing (17)
  • unify-front-end/app/(tabs)/Checklist/index.tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/intro/[pageNum].tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/lessons/[lessonId]/activities/[pageNum].tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/lessons/[lessonId]/ending/[pageNum].tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/lessons/[lessonId]/pages/[pageNum].tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/lessons/[lessonId]/quizzes/[quizId]/pages/[questionNum].tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/practice/[practiceId]/activity/[pageNum].tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/practice/[practiceId]/pages/[questionNum].tsx
  • unify-front-end/app/(tabs)/Learn/modules/[moduleId]/[submoduleId]/tasks/[taskId]/index.tsx
  • unify-front-end/app/(tabs)/_layout.ios.tsx
  • unify-front-end/app/(tabs)/companion/index.tsx
  • unify-front-end/constants/Routes.ts
  • unify-front-end/hooks/useTabBarHeight.ts
  • unify-front-end/i18n/locales/en/translation.json
  • unify-front-end/i18n/locales/es/translation.json
  • unify-front-end/i18n/locales/hi/translation.json
  • unify-front-end/i18n/locales/vi/translation.json

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'skip_files'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-tabs-ios

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Savar-G Savar-G self-assigned this May 19, 2026
Savar-G and others added 5 commits May 18, 2026 18:24
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>
@Savar-G
Savar-G marked this pull request as draft August 16, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant