feat: TabsScrollArea + TabsScrollAreaIndicator#586
feat: TabsScrollArea + TabsScrollAreaIndicator#586adrienzheng-cb wants to merge 6 commits intomasterfrom
Conversation
🟡 Heimdall Review Status
🟡
|
| Code Owner | Status | Calculation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ui-systems-eng-team |
🟡
0/1
|
Denominator calculation
|
cb-ekuersch
left a comment
There was a problem hiding this comment.
I see the scrolling functionality was removed from TabbedChips but I guess the main thing im thinking is why? Also isn't NavigationTabs deprecated? I saw it mentioned in the PR description as being a motivating reason for these changes
| onActiveTabElementChange: (element: View | null) => void; | ||
| }; | ||
|
|
||
| export type TabsScrollAreaBaseProps = Omit<BoxBaseProps, 'children' | 'ref'> & |
There was a problem hiding this comment.
dang we should probably remove 'children' from BoxBaseProps at some point
| * Render function that receives `onActiveTabElementChange` (wire to `Tabs` as | ||
| * `onActiveTabElementChange`). | ||
| */ | ||
| children: (props: TabsScrollAreaRenderProps) => React.ReactNode; |
There was a problem hiding this comment.
children should go on Props not BaseProps - @hcopp wdyt? I recall us coming to that conclusion the other day
There was a problem hiding this comment.
moved children to full props
| }; | ||
|
|
||
| export type TabsScrollAreaProps = TabsScrollAreaBaseProps & { | ||
| styles?: { |
There was a problem hiding this comment.
use the styles/classNAmes helper type
There was a problem hiding this comment.
we should make an internal lint rule to enforce this
There was a problem hiding this comment.
I will add lint rule in a separate pr
| }; | ||
|
|
||
| const TabsScrollAreaWithRef = forwardRef<View, TabsScrollAreaProps>( | ||
| function TabsScrollArea(_props, ref) { |
There was a problem hiding this comment.
just curious, why can't this be a feature of Tabs, enabled by certain props? Having a separate component feels a bit heavy just to support scrolling with some opinionated overflow styles
There was a problem hiding this comment.
I thought about doing it that way and it would be very ideal. however:
-
Tabs component is a root div wrapping a list of tab, we cannot add additional paddle/overflow indicator inside the root div because the div has a role of "tablist" and having buttons inside makes it inaccessible
-
we don't want to add additional layers outside of the current root of Tabs because Tabs are already used by consumers and that can lead to drastic behavioral changes and may break their styling if they applies complex custom styles.. We also want to avoid wrapping the component in additional layer based on a
scrollableprop conditionally
There was a problem hiding this comment.
Tabs component is a root div wrapping a list of tab, we cannot add additional paddle/overflow indicator inside the root div because the div has a role of "tablist" and having buttons inside makes it inaccessible
So do customers use either Tabs or TabScrollArea depending on if they want scroll behavior?
Re: Tabs not rendering TabsScrollArea - why can't TabsScrollArea implement tablist role itself and then in Tabs if it decides it needs to render TabsScrollArea it doesn't apply the role to it's own container?
There was a problem hiding this comment.
TabsScrollArea is just an add-on to Tabs. It does not render the Tabs itself, it is just a scrollable Box that detects horizontal overflow and displays overflow indicators when overflow happens.
We didn't build this behavior into Tabs because Tabs are currently being used by consumers, and we want to avoid breaking their existing usages. One solution to that is to add an optional scrollable prop to Tabs that only add the overflow detection to it if enabled. However, Tabs' DOM structure becomes inconsistent when the prop is turned on/off if we go with that route. In the past we had issues which components that adds additional layer of Pressable around itself based on availability of certain props. (eg Cell) and we have been trying to avoid that pattern since.
Hence, I decided to keep TabsScrollArea as a separate component and leave Tabs as it is (a primitive component that only takes care of the business logic).
This way, existing usages of Tabs in consumer repos are unaffected by this change. They probably have implemented their own overflow strategy if they needed it. Anyone who are still using the Deprecated TabNavigation or would like to add scroll support to Tabs can switch to the following
<TabsScrollArea>
{
({ onActiveTabElementChange }) => <Tabs onActiveTabElementChange={onActiveTabElementChange} {...otherProps} />
}
</TabsScrollArea>
| style?: React.CSSProperties; | ||
| /** Merged with the root `HStack`. */ | ||
| className?: string; | ||
| styles?: { |
There was a problem hiding this comment.
use style and classNames type helper
cc:@cb-ekuersch We didn't remove it we just replaced the old scrolling implementation inside TabbedChips with the new TabsScrollArea. TabsScrollArea is essentially the old scroll logic from TabbedChips and TabNavigation extracted into its own component. We did deprecate TabNavigation, however, Tabs (supposedly the replacement) was too primitive to provide the same functionalities TabNavigation used to offer and TabNavigation was heavily used. We had consumers complaining that it was hard to migrate to Tabs because it didn't have Tab, ActiveIndicator, or scroll indicators built in. I recently added DefaultTab and DefaultTabsActiveIndicator in a previous pr, and this pr is the last piece of puzzle for TabNavigation users to easily migrate to Tabs. |
What changed? Why?
Summary
⭐️⭐️⭐️ This change fully bridges the gap between Tabs and TabNavigation. ⭐️⭐️⭐️
Introduces
TabsScrollAreaon web and mobile as the supported way to host a horizontally scrollingTabsrow when content overflows. The API uses a render prop (childrenas a function) so consumers passonActiveTabElementChangeintoTabsfor scroll-into-view behavior. AlphaTabbedChipsis updated to composeTabsScrollAreaso chip filters get the same overflow behavior and shared styling hooks.Web (
packages/web)TabsScrollAreaHStack; root props are based onBoxBaseProps(and related types) so layout props (e.g.width,maxWidth) can be set on the component without an extra wrapper.onActiveTabElementChangefor wiring toTabs.TabsScrollAreaOverflowIndicator(fade gradient + chevron controls).compactonTabsScrollAreafor smaller default overflow **IconButton**s (used whenTabbedChipspassescompactthrough).OverflowIndicatorComponentto replace the default indicator;styles/classNamesslots for root, scroll container, and overflow subparts.TabbedChips(alpha)TabsScrollArea; mapsstyles/classNamesintoTabsScrollAreaslots (including deprecatedstyles.paddlemerged into overflow button styles where applicable for backward compatibility).tabs/index.tsexportsTabsScrollArea/ overflow indicator types as needed.componentConfigTabsScrollAreafor theme/config overrides.Mobile (
packages/mobile)TabsScrollAreaScrollViewfor the tab row; root layout usesHStack(or equivalent stack) with **BoxBaseProps-style props for dimensions on the root.TabsScrollAreaOverflowIndicatorimplemented withOverflowGradientand an explicitdirection('left'|'right') API aligned with web semantics.OverflowGradientupdated to support the pinning/gradient direction needed for left/right edges.children: ifchildrenis not a function, the component does not throw; it renders non-function children (or nothing) so misuse fails softly (scroll-into-view wiring is skipped in that case).TabbedChips(alpha)TabsScrollAreaand passes through overflow-related styles.componentConfigTabsScrollAreaentry incomponentConfig.Documentation (
apps/docs)components/navigation/TabsScrollArea/index.mdx, web/mobile examples, props tables, styles tables,webMetadata.json/mobileMetadata.json(short descriptions, related components such as Tabs / TabbedChips where applicable).width/maxWidthonTabsScrollAreainstead of an extraBoxwrapper where appropriate.docgen.config.jstabs/TabsScrollAreafor API extraction.sidebars.tsTabsdocsApps
apps/mobile-appUI changes
storybook
docs
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2026-04-03.at.17.02.27.mov
Screen.Recording.2026-04-03.at.5.03.14.PM.mov
Testing
How has it been tested?
Testing instructions
Illustrations/Icons Checklist
Required if this PR changes files under
packages/illustrations/**orpackages/icons/**Change management
type=routine
risk=low
impact=sev5
automerge=false