Redesign the mobile app iOS-first with native chrome and Liquid Glass - #2457
Merged
Conversation
The Expo app was a web-parity port: Inter/Fira Code, Hugeicons, an opaque header in theme.css colors, bottom sheets for every menu, bordered web cards. This makes it read as a native iOS app while keeping Android on its existing paths and custom palettes working. Foundation - System fonts (SF Pro / Menlo, sans-serif / monospace on Android); the Google-font packages and the splash font gate are gone. Apple text ramp in the generated theme (11 · 13 · 15 · 17 · 20 · 22 · 28 · 34). - A mobile-only override layer in scripts/generate-native-theme.ts retunes the default palette to iOS values (label, separators, grouped surfaces, systemBlue tint, system reds/greens) and adds --surface-grouped(-cell). Tokens stay strings; palettes still tint through --canvas/--ink. - SF Symbols through the existing Icon on iOS (src/ui/Icon.ios.tsx + sf-symbol-map.ts); Hugeicons stay on Android and for brand marks. - @expo/ui 57.0.11 (exact; its pod was already linked via expo-router) for the native segmented control and icon-button menus. - Primitives: GroupedSection/GroupedRow/IconBadge, iOS buttons, fields, untinted system switch, UIKit-look sheets, confirmDestructive (Alert), promptName (Alert.prompt), ScreenTitle. Surfaces - Transparent headers with large titles on list screens (iOS 26 draws its own bar; older iOS gets the chrome material), Stack.Toolbar menus for the workspace switcher, display options, thread actions and settings overflow, Stack.SearchBar on Home / Plugins / Skills, modal presentation for New project and Connect (with their own sheet host). - Home: 17pt rows under contentInsetAdjustmentBehavior automatic, inset hairlines, SF status glyphs, sentence-case sections, Mail-style swipe actions, search results in place. - Thread: tinted continuous-corner user bubble, native bar buttons, iOS-style interaction banners and queue, iMessage-style composer with SF buttons and capsule option pills. - Settings, machines, plugins, skills, projects, connect as inset-grouped screens with icon badges and grouped forms; panel / files / terminal restyled (pill tab bar, iOS search field, keyboard-accessory key bar). Verified on device: expo-router Link.Menu/Link.Preview and @expo/ui MenuView remove the wrapped RN subtree from the iOS accessibility tree, so text-bearing rows and pills use Pressable + the restyled sheets; native menus remain only on labeled icon-only buttons and header items (src/ui/README.md documents the rule). New guard tests pin the SF Symbol map, platform neutrality (iOS-only APIs in *.ios.tsx or behind guards; siblings never import their own basename) and the font tables. Maestro flows updated for the native header items. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
…quid Glass
On device the list screens scrolled under a static large title and search
bar, and the composer read as a flat card.
Large titles never collapsed because Fabric's view flattening turned the
Screen root View (background + testID) into a childless sibling at
subviews[0]; UIKit and react-native-screens look for the content scroll view
along the first-subview chain, so the lookup dead-ended on every screen
(verified with an lldb recursiveDescription dump). The scroll branch now
renders the ScrollView as the screen root and the list branch keeps its
root with collapsable={false}. With the scroll view adopted, iOS 26 draws
its own glass scroll-edge effect under the compact bar, so the header stays
transparent with no blur material (the Liquid Glass guidance: no custom bar
backgrounds); older iOS keeps the chrome material.
The composer is a GlassSurface (expo-glass-effect GlassView on iOS 26+, the
previous card elsewhere) floating over the list and the timeline: the home
dock and the thread prompt area become transparent overlays whose measured
height feeds the list's bottom inset, scroll indicators and the
jump-to-latest pill; floating banners and the chip stack sit on a raised
opaque panel so forms stay readable over the timeline. expo-glass-effect is
now a direct dependency (its pod was already linked through expo-router);
the platform-neutrality test keeps its import inside the .ios.tsx sibling.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
…meline iOS 26's default soft scroll-edge effect is a gradient fade that still lets rows read through the bar. List screens now ask for the hard edge style (the Liquid Glass guidance for dense lists): an opaque bar region with a hairline while the large title and bar items stay glass. The thread timeline gets an opaque inline header with a hairline instead, so nothing scrolls under the title and status line. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
The bar is now a frosted material instead of opaque or transparent: a chrome material on the compact bar, with the large-title area transparent at rest. The adaptive material resolves to its light flavor inside the bar on iOS 26 regardless of the app's scheme, so the dark/light flavor is picked from the theme mode (which also honors an in-app override); the system scroll-edge effect is hidden so the two do not stack. The thread timeline uses the same frosted bar instead of an opaque one. The prompt chips above the composer no longer sit on a solid panel: each chip is a Liquid Glass capsule floating over the timeline (foreground-toned detail text for legibility); only the pending-interaction form and the queued list keep a raised opaque panel. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
The thread list and timeline bars rendered as a flat slab: with no react-navigation ThemeProvider the native stack assumed a light theme and set the UINavigationBar's overrideUserInterfaceStyle to light, so every adaptive material blurred in its light flavor over the dark app (hence the pale block behind large titles) and the explicit *Dark flavors had to be hand-picked. Provide the navigation theme from the app theme (mode + tokens) so the bar trait follows the in-app mode, then use the adaptive ultra-thin material: content now measurably blurs through the bar in both modes instead of sitting under a near-opaque chrome fill. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
The frosted bar still read as a flat tinted band on the phone: a UIKit blur material over mostly white or black content has nothing to show through, so it looks like a solid panel next to the composer's glass. Inline-title bars now get the composer's material: the native stack renders `headerBackground` as a view sized to the bar behind a transparent UINavigationBar, and `HeaderGlass` fills it with an oversized, clipped `GlassSurface` so only the bottom edge of the glass shows. The stack's absolute-fill background view lays out at zero height, so the clip takes the bar height from the header-height context explicitly. Large-title bars keep a native blur (the classic `regular` frost) because the stack debounces their height while the title collapses; hidden and opaque bars drop the background. The thread title view was capped at 240pt: UIKit centers a custom title view at the size we lay out, so long titles ran under the right-hand glass group. Cap it to the room between the bar items instead; UIKit shifts a narrower view off-center to fit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
SawyerHood
force-pushed
the
sawyer/mobile-ios-native-redesign
branch
from
August 26, 2026 01:01
7323f5d to
8be68d4
Compare
SawyerHood
marked this pull request as ready for review
August 26, 2026 01:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The Expo app was a web-parity port: Inter/Fira Code, Hugeicons, an opaque header painted in
theme.csscolors, bottom sheets for every menu, bordered web cards. It never read as a native iOS app, and on iOS 26 it ignored the system chrome entirely (no large titles, no native bar items or search, no Liquid Glass). Along the way three real bugs surfaced that made the native chrome impossible to adopt: large titles never collapsed (Fabric view flattening turned the screen root into a childless leaf, so UIKit's scroll-view lookup dead-ended), the navigation bar was forced to the light trait (no react-navigation theme was ever provided, so every adaptive material rendered its light flavor over a dark app), and long thread titles ran under the right-hand bar buttons.What changed
apps/mobileonly; no server, daemon, CLI, or wire changes.Foundation
scripts/generate-native-theme.tsretunes the default palette to iOS values (label, separators, grouped surfaces, systemBlue tint). Tokens stay strings; custom palettes still tint through--canvas/--ink.Iconon iOS (src/ui/Icon.ios.tsx+sf-symbol-map.ts); Hugeicons stay on Android and for brand marks.@expo/ui57.0.11 (native segmented control, icon-button menus) andexpo-glass-effectas direct dependencies (both pods were already linked via expo-router).GroupedSection/GroupedRow/IconBadge, iOS buttons and fields, untinted system switch, UIKit-look sheets,confirmDestructive(Alert),promptName(Alert.prompt),ScreenTitle,GlassSurface,NativeMenu.Navigation chrome (
src/screens/shell/)RootNavigatorprovides a react-navigation theme derived from the app theme (mode + tokens), so theUINavigationBartrait follows the in-app light/dark mode.headerBackgroundrendersHeaderGlass, an oversized, clippedGlassSurfacesized from the header-height context (an absolute-fill child of the stack's background view lays out at zero height). Large-title screens keep a nativeregularblur because the stack debounces their bar height while the title collapses; hidden/opaque bars drop the background. Older iOS keeps the chrome material; Android keeps an opaque bar.Screenrenders theScrollViewas the screen root (list branch:collapsable={false}) so UIKit and react-native-screens find the content scroll view and large titles collapse.Stack.Toolbarmenus for the workspace switcher, display options, thread actions and settings overflow;Stack.SearchBar(stacked placement) on Home / Plugins / Skills; modal presentation for New project and Connect with their own sheet host.Screens
contentInsetAdjustmentBehavior="automatic", inset hairlines, SF status glyphs, Mail-style swipe actions, in-place search results.GlassSurfacewhose measured height feeds the list inset, scroll indicators and the jump-to-latest pill; prompt chips are glass capsules. The title is capped to the room between the bar items (UIKit centers a custom title view at the laid-out size and never shrinks it).Accessibility finding, verified on device: expo-router
Link.Menu/Link.Previewand@expo/uiMenuViewremove the wrapped RN subtree from the iOS accessibility tree (VoiceOver and UI automation cannot find the rows). Text-bearing rows and pills therefore usePressable+ the restyled sheets; native menus remain only on labeled icon-only buttons and header items.src/ui/README.mddocuments the rule.How you verified
pnpm exec turbo run typecheck lint test --filter=@bb/mobile: clean, 127 files / 888 tests pass. New guard tests pin the SF Symbol map, the font tables, and platform neutrality (iOS-only APIs live in*.ios.tsxor behind guards;expo-glass-effectonly inGlassSurface.ios.tsx; siblings never import their own basename — the self-import case stack-overflowed every route once).expo exportfor iOS and Android succeeds.apps/mobile/e2e) updated for the native header items and run against the seeded harness backend (BB_MOBILE_E2E_PORT=41999 pnpm exec turbo run e2e:mobile-backend --filter=@bb/integration-tests) on an iPhone 17 Pro / iOS 26 simulator.GroupedRowswallowing the Switch for VoiceOver, keyboard dismiss mode, double bottom insets, …), all fixed.Still open, tracked for follow-up: an Android emulator pass, an upstream issue for the
Link.Menu/MenuViewaccessibility behavior, a designed dark splash icon, a dimmer search placeholder, and light-mode--subtle-foregroundcontrast. The branch is behindmainand will be rebased before review.No tracking issue; this started from a design request.