Skip to content

Conversation

@georgewrmarshall
Copy link
Contributor

@georgewrmarshall georgewrmarshall commented Nov 6, 2025

Description

This PR significantly refactors the Tabs component to reduce complexity improve maintainability. The refactoring removes ~200 lines of complex animation code while introducing a simpler, more performant animation system.

What is the reason for the change?

  1. Complex animation system: TabsBar tracked individual tab layouts and manually animated an underline across the bar using RAF callbacks and complex state management
  2. Unnecessary complexity: Multiple refs, layout tracking, and animation state that made the code hard to maintain
  3. Performance concerns: The complex animation system with layout measurements could impact performance

What is the improvement/solution?

  1. Self-contained Tab animation: Each Tab now renders its own animated underline that grows/shrinks from center using a simple scaleX transform
  2. Simplified scroll detection: Moved padding to ScrollView's contentContainerStyle so scroll detection works automatically without hardcoded values
  3. Removed animation complexity: Eliminated 199 lines of animation-related code from TabsBar (66% of the component)
  4. Cleaner component structure: Removed unnecessary wrapper Views and unused props
  5. Performance improvements: Uses native driver for animations and eliminates RAF callbacks

Changelog

CHANGELOG entry: null

Related issues

Fixes: N/A (internal refactoring)

Manual testing steps

Feature: Tabs component refactoring

  Scenario: user interacts with tabs
    Given the app is open with a screen containing TabsBar component
    
    When user taps on different tabs
    Then the underline should smoothly animate from center (grow out) on the active tab
    And the previous tab's underline should smoothly shrink to center
    And tab switching should feel responsive and smooth
    
  Scenario: user views tabs with overflow
    Given a TabsBar with many tabs that overflow the container width
    
    When the content width exceeds container width
    Then horizontal scrolling should be automatically enabled
    And scrolling should work smoothly
    
  Scenario: user interacts with disabled tab
    Given a TabsBar with some disabled tabs
    
    When user taps on a disabled tab
    Then nothing should happen
    And the underline should not animate

Screenshots/Recordings

Before

Complex animation system with manual underline positioning across the entire TabsBar

tabsbefore.mov

After

Simplified animation with each tab handling its own underline, growing from center when active

after480.mov

Performance Impact

Yes, this improves performance:

  1. Native driver animations: The new animation uses useNativeDriver: true, which runs animations on the native thread instead of the JS thread, providing 60fps animations even when JS is busy
  2. Eliminated RAF callbacks: Removed requestAnimationFrame usage that was previously polling for layout changes
  3. Reduced layout measurements: No longer tracking individual tab layouts and container positions
  4. Simpler component tree: Removed unnecessary View wrappers and refs
  5. Reduced re-renders: Eliminated complex state management that could trigger unnecessary re-renders

Code reduction:

  • TabsBar: 300 lines → 87 lines (71% reduction)
  • Removed 199 lines of animation code
  • Net change: -168 lines of code

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Reworks Tabs to use per-Tab underline animation and simplified scroll detection, removing complex layout/animation logic from TabsBar.

  • Components:
    • Tabs/Tab:
      • Replaces wrapper View with Pressable and adds animated underline (Animated.View) using scaleX with native driver.
      • Uses hidden bold text for sizing; visible text overlays with conditional weights/colors; respects isDisabled.
      • Removes onLayout handling and related props; imports AnimationDuration.
    • Tabs/TabsBar:
      • Removes animated underline, RAF/layout tracking, and multiple refs/state; trims component API (uses style, drops ...boxProps).
      • Always renders a horizontal ScrollView with padding/gap in contentContainerStyle and enables scrolling based on container width vs content size.
  • Types:
    • Tab.types.ts: Drops onLayout prop and LayoutChangeEvent import.
  • Tests:
    • Snapshot updates reflecting per-Tab underline, simplified structure, and scroll container.

Written by Cursor Bugbot for commit 9bf41a3. This will update automatically on new commits. Configure here.

@georgewrmarshall georgewrmarshall requested a review from a team as a code owner November 6, 2025 22:23
@github-actions
Copy link
Contributor

github-actions bot commented Nov 6, 2025

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-design-system All issues relating to design system in Mobile label Nov 6, 2025
@github-actions github-actions bot added the size-M label Nov 6, 2025
@georgewrmarshall georgewrmarshall force-pushed the refactor-tabsbar-reduce-complexity branch from b9e821f to 9bf41a3 Compare November 6, 2025 22:26
/**
* Callback when tab layout changes
*/
onLayout?: (event: LayoutChangeEvent) => void;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used

@georgewrmarshall georgewrmarshall marked this pull request as draft November 6, 2025 22:30
@georgewrmarshall georgewrmarshall changed the title refactor: simplify Tabs component and improve performance refactor: simplify Tabs component Nov 6, 2025
@georgewrmarshall georgewrmarshall self-assigned this Nov 6, 2025
Comment on lines -28 to -35
const handleOnLayout = useCallback(
(layoutEvent: Parameters<NonNullable<typeof onLayout>>[0]) => {
if (onLayout) {
onLayout(layoutEvent);
}
},
[onLayout],
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed

Comment on lines +29 to +43
useEffect(() => {
// Skip animation on initial mount - just set the value
if (isInitialMount.current) {
isInitialMount.current = false;
scaleAnim.setValue(isActive && !isDisabled ? 1 : 0);
return;
}

// Animate on subsequent changes
Animated.timing(scaleAnim, {
toValue: isActive && !isDisabled ? 1 : 0,
duration: AnimationDuration.Fast,
useNativeDriver: true,
}).start();
}, [isActive, isDisabled, scaleAnim]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handles animation of underline

Comment on lines -38 to -41
<View
ref={viewRef}
onLayout={handleOnLayout}
style={tw.style('flex-shrink-0')}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reducing element bloat by removing wrapping View as it's no longer needed

Comment on lines +85 to +93
{/* Animated underline */}
<Animated.View
style={[
tw.style('absolute bottom-0 left-0 right-0 h-0.5 bg-icon-default'),
{
transform: [{ scaleX: scaleAnim }],
},
]}
/>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved underline into the Tab component. A bit of flexibility on the animation style here

Comment on lines +17 to +30
<RCTScrollView
contentContainerStyle={
{
"flexDirection": "row",
"gap": 24,
"paddingLeft": 16,
"paddingRight": 16,
}
}
horizontal={true}
onContentSizeChange={[Function]}
scrollEnabled={false}
scrollsToTop={false}
showsHorizontalScrollIndicator={false}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default renders the ScrollView so we get quite large snapshot updates

@@ -1,20 +1,10 @@
// Third party dependencies.
import React, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we can remove all the complex logic that handled the underline animation

Comment on lines +61 to 81
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
scrollEnabled={scrollEnabled}
style={tw.style('flex-grow-0')}
contentContainerStyle={tw.style('flex-row px-4 gap-6')}
scrollsToTop={false}
onContentSizeChange={handleContentSizeChange}
>
{tabs.map((tab, index) => (
<Tab
key={tab.key}
label={tab.label}
isActive={index === activeIndex}
isDisabled={tab.isDisabled}
onPress={() => handleTabPress(index)}
testID={`${testID}-tab-${index}`}
/>
))}
</ScrollView>
</Box>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing a lot of duplicated code and the scrollEnabled in favor of scrollEnabled via the ScrollView. We check if this is enabled by checking the content of ScrollView against the width of the wrapping Box component which is calculated via the handleContainerLayout

georgewrmarshall and others added 3 commits November 7, 2025 10:47
Fixes a bug where the Tab animation would run on initial mount even when
already at the target state. This caused unnecessary animation work and
potential visual glitches.

Changes:
- Added isInitialMount ref to track first render
- Skip animation on mount, immediately set value instead
- Subsequent changes animate normally

This ensures underlines appear instantly on first render and only animate
when the active state actually changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@georgewrmarshall georgewrmarshall force-pushed the refactor-tabsbar-reduce-complexity branch from 6eef084 to 17c3018 Compare November 7, 2025 18:48
@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 7, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size-M team-design-system All issues relating to design system in Mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants