Repository navigation
Conversation
A screen that pages between tabs has had to draw the tab row in PHP. That row can only change when PHP sends a frame, so it lags the swipe, and sending a frame mid-swipe is what makes the swipe stutter. tabs="[...]" draws the row natively above the pages, one label per page. The row reads the pager's scroll position every frame. The indicator slides between the labels either side of it, and the highlight moves to the nearest page, so it changes as the swipe passes halfway. Tapping a tab scrolls the pager there and the page is reported when it settles, as for a swipe. Tapping the tab already showing calls on-tab-reselect with its index instead, so the page can go back to its top. By default the tabs share the row equally. tabs-scrollable makes them as wide as their labels in a row that scrolls to keep the current one in the middle. Colours default to the theme (on-surface, on-surface-variant, primary, outline-variant). The tab-* attributes override the colours, font, indicator thickness and padding per pager, and a theme-<token> colour sends its dark shade too. iOS keeps the position in an observable that only the row reads, so a swipe redraws the row and not the pages. Android draws the indicator from currentPage plus currentPageOffsetFraction in the draw phase. The tabs are tabs to VoiceOver and TalkBack, with the selected one marked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
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.
Stacked on #135. Review that one first. Docs: NativePHP/nativephp.com#563.
A screen that pages between tabs has to draw its tab row in PHP today. The row can only change when PHP sends a frame, so it trails the swipe. Sending that frame during the swipe is also what makes the swipe stutter (#135). So the row has to be native and driven by the pager itself.
The pager draws the row above its pages, one label per page.
on-tab-reselectwith its index instead, so the app can take that page back to its top.tabs-scrollablemakes them as wide as their labels, in a row that scrolls to keep the indicator in the middle as you swipe.None of that waits for PHP.
Look
Colours come from the theme by default:
on-surfacefor the active label,on-surface-variantfor the others,primaryfor the indicator andoutline-variantfor the hairline under the row. The font is the theme's atfont-md.These attributes change it per pager:
tab-active-color,tab-coloron-surface,on-surface-varianttab-indicator-colorprimarytab-divider-coloroutline-variant,transparenthides ittab-font,tab-font-sizefont-mdtab-indicator-heighttab-padding-x,tab-padding-top,tab-padding-bottomtab-edge-paddingThe defaults make a 48pt tab. Colours take the usual palette and hex values, and
theme-<token>too, which also sends the token's dark shade. A tab can be['label' => ..., 'a11y-label' => ...]when VoiceOver and TalkBack should say more than the label.The repo's components are normally theme-only. The colour attributes are here because the row sits on top of whatever the pages are. A video feed needs light labels and a list doesn't, on the same app theme. That's the same reason
activity-indicatortakes a colour.How it works
iOS:
PagerBodywrites the unrounded scroll position into an@Observableobject on every geometry change. Only the strip reads it, so a swipe redraws the strip and not the pages. Labels are measured withonGeometryChange. A scrollable row follows withScrollPosition.scrollTo(x:). A tap sets the pager'sscrollPositioninsidewithAnimation, the same binding PHP's jumps use.Android: the indicator is drawn in
drawBehindfromcurrentPage + currentPageOffsetFraction, so a swipe only invalidates drawing. The highlight iscurrentPage. A tap callsanimateScrollToPage. A scrollable row follows withscrollState.scrollTo.Both: the tabs are tabs to VoiceOver and TalkBack, with the selected one marked. The pager's
a11y-labelnow applies to the pages only, not the row.Tests
PagerTabsTestcovers the props,theme-colours with both shades, the fluent API, and checks the Swift and Kotlin source like the other iOS tests.How I tested
A test app from mobile-starter with devnull's theme and Geist fonts. One screen has three fixed tabs, one has seven scrollable ones, and one draws devnull's current PHP tabs for comparison. Every page report and render is logged with a timestamp. iPhone 17 Pro simulator on iOS 27.0, and an Android 15 emulator.
reselectand the app's keyed list goes back to the top.On iOS, a pager that opens on a page other than 0 also needs #131, which fixes the start page. Without it the pager and the row open on page 0. The dev-null app branch has both.
Not tested: a physical device, iPad, landscape, right-to-left layouts, and tabs on a vertical pager (it draws them, but I didn't try it).
Screenshots
iOS, both layouts. Slow drags, taps, and a reselect, with the idle time between steps cut:
devnull's PHP tabs above, the native row below, light and dark:
A slow drag, a frame every 0.2s. The highlight flips between the fourth row's first and second frames:
The scrollable row following a swipe from Quotes to Stars:
Android, fixed and scrollable:
The images are on the orphan branch
pager-tabs-screenshots. Delete it after merge.🤖 Generated with Claude Code