Skip to content

fix(animated-fab): keep icon attached when toggling iconMode on iOS#4940

Open
azizbecha wants to merge 1 commit into
callstack:mainfrom
azizbecha:fix/animated-fab-icon-mode-toggle
Open

fix(animated-fab): keep icon attached when toggling iconMode on iOS#4940
azizbecha wants to merge 1 commit into
callstack:mainfrom
azizbecha:fix/animated-fab-icon-mode-toggle

Conversation

@azizbecha
Copy link
Copy Markdown
Collaborator

Summary

On iOS only, AnimatedFAB's icon visually detaches from the FAB pill after the iconMode prop is switched from static → dynamic → static. The icon ends up floating off to the side, offset by roughly textWidth + borderRadius, instead of staying centered in the collapsed pill.

Root cause

In getCombinedStyles() (src/components/FAB/utils.ts), the icon wrapper's transform.translateX was set conditionally to either an Animated.Value (dynamic mode) or a plain JS number (static mode):

translateX: isIconStatic ? 0 : animFAB

Animations run with useNativeDriver: true. When a style prop that was being driven by the native animated module is replaced with a plain value, the native side doesn't reliably release ownership of that prop, so the icon's view stays stuck at animFAB's last value (e.g. the extended distance). Combined with the always-applied left: -distance, the icon lands in the wrong place — detached from the pill. On Android this swap behaves fine, which is why the bug is iOS‑specific.

Fix

Never swap a style prop between an Animated node and a plain value. iconWrapper.translateX is now driven by animFAB.interpolate(...) in all animateFrom / RTL branches, using a constant output range for the non‑animating case, so the prop is always owned by the native driver and updates correctly on every iconMode / extended change. The animateFrom="left" + RTL dynamic branch was also normalized to a constant interpolation to match its previous static value of -distance.

No changes needed in AnimatedFAB.tsx, left / right stay plain numbers (never animated, so no swap problem there).

Testing

  • yarn typescript — clean
  • yarn jest — full suite passes (901 passed, 164 snapshots; no snapshot churn — the existing snapshots cover iconMode="dynamic", which
    is unchanged)
  • yarn lint — clean
  • Manual (iOS simulator, Animated FAB example): toggle iconMode static → dynamic → static with extend/collapse scrolling in between — icon stays centered in the collapsed pill. Also verified with animateFrom="left".
Screen.Recording.2026-05-11.at.4.31.56.PM.mov

The icon's transform.translateX was conditionally either an Animated.Value
(dynamic) or a plain number (static). With useNativeDriver, switching the
prop from an animated node to a plain value doesn't reliably release it
from the native animated module, leaving the icon stuck at animFAB's last
value and visually detached from the FAB pill after static -> dynamic ->
static.

Drive iconWrapper translateX through animFAB.interpolate(...) in all
animateFrom/RTL branches, using a constant output range for the
non-animating case, so the prop is always owned by the native driver.
@azizbecha azizbecha requested a review from ruben-rebelo May 11, 2026 15:45
@callstack-bot
Copy link
Copy Markdown

Hey @azizbecha, thank you for your pull request 🤗. The documentation from this branch can be viewed here.

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.

2 participants