refactor: rework Surface and elevation shadows - #5078
Conversation
There was a problem hiding this comment.
Pull request overview
Reworks Surface around Reanimated-compatible styling and updates dependent components, animations, examples, documentation, and tests.
Changes:
- Replaces layered/style-flattening Surface behavior with explicit visual props and platform shadows.
- Migrates several component animations to Reanimated.
- Updates affected APIs, tests, snapshots, examples, and documentation.
Reviewed changes
Copilot reviewed 61 out of 70 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/utils/animations.ts |
Removes obsolete Animated helper. |
src/theme/tokens/sys/elevation.ts |
Reworks static shadow generation. |
src/components/ToggleButton/ToggleButton.tsx |
Updates animated style typing. |
src/components/Surface.tsx |
Reimplements Surface and visual props. |
src/components/Snackbar.tsx |
Migrates animations to Reanimated. |
src/components/Searchbar.tsx |
Adopts new Surface API. |
src/components/Modal.tsx |
Adds transitions and elevation. |
src/components/Menu/Menu.tsx |
Migrates menu animations and layout. |
src/components/IconButton/IconButton.tsx |
Replaces Surface with Animated View. |
src/components/FAB/useVisibility.ts |
Removes superseded visibility hook. |
src/components/FAB/Shell.tsx |
Integrates Surface and visibility animation. |
src/components/FAB/Extended.tsx |
Updates measurement behavior. |
src/components/Dialog/Dialog.tsx |
Uses Modal elevation. |
src/components/Chip/Chip.tsx |
Adopts numeric elevation transitions. |
src/components/Card/Card.tsx |
Adopts new Surface visual props. |
src/components/Button/Button.tsx |
Adopts new Surface visual props. |
src/components/BottomNavigation/BottomNavigationBar.tsx |
Adds animated wrapper around Surface. |
src/components/Banner.tsx |
Migrates animations to Reanimated. |
src/components/Appbar/utils.ts |
Updates elevation and border helpers. |
src/components/Appbar/AppbarHeader.tsx |
Replaces elevated flag with elevation. |
src/components/Appbar/Appbar.tsx |
Integrates new Surface API. |
src/components/__tests__/ToggleButton.test.tsx |
Removes legacy animation test. |
src/components/__tests__/Surface.test.tsx |
Updates Surface layer assertions. |
src/components/__tests__/Snackbar.test.tsx |
Removes legacy animation test. |
src/components/__tests__/Searchbar.test.tsx |
Removes legacy animation test. |
src/components/__tests__/Modal.test.tsx |
Updates Surface and style assertions. |
src/components/__tests__/Menu.test.tsx |
Updates content-style assertion. |
src/components/__tests__/IconButton.test.tsx |
Removes legacy animation test. |
src/components/__tests__/Chip.test.tsx |
Removes legacy animation test. |
src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap |
Refreshes Reanimated snapshots. |
src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap |
Refreshes Reanimated snapshots. |
src/components/__tests__/Card/Card.test.tsx |
Removes legacy animation test. |
src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap |
Reflects new Surface structure. |
src/components/__tests__/Button.test.tsx |
Removes legacy animation test. |
src/components/__tests__/BottomNavigation.test.tsx |
Targets new animated wrapper. |
src/components/__tests__/Banner.test.tsx |
Removes legacy animation test. |
src/components/__tests__/Appbar/Appbar.test.tsx |
Updates Appbar API assertions. |
src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap |
Reflects IconButton structure. |
src/components/__tests__/__snapshots__/Switch.test.tsx.snap |
Refreshes Reanimated snapshots. |
src/components/__tests__/__snapshots__/ListItem.test.tsx.snap |
Reflects new Chip Surface structure. |
src/components/__tests__/__snapshots__/Badge.test.tsx.snap |
Refreshes Reanimated snapshots. |
jest/testSetup.js |
Adjusts worklets mocking. |
example/src/RootNavigator.tsx |
Updates Appbar usage. |
example/src/Examples/TooltipExample.tsx |
Updates Appbar usage. |
example/src/Examples/TeamDetails.tsx |
Updates Appbar usage. |
example/src/Examples/SurfaceExample.tsx |
Demonstrates new Surface props. |
example/src/Examples/MenuExample.tsx |
Updates Appbar usage. |
example/src/Examples/BottomNavigationExample.tsx |
Updates Appbar usage. |
example/src/Examples/AppbarExample.tsx |
Uses numeric elevation. |
docs/6.x/docs/components/Surface.mdx |
Documents Surface visual props. |
docs/6.x/docs/components/Modal.mdx |
Documents Modal elevation. |
docs/6.x/docs/components/Chip/Chip.mdx |
Documents Chip ref. |
docs/6.x/docs/components/Card/Card.mdx |
Documents Card ref. |
docs/6.x/docs/components/Appbar/AppbarHeader.mdx |
Documents elevation API. |
docs/6.x/docs/components/Appbar/Appbar.mdx |
Documents elevation API. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| height: shadowLayers[layer].height[elevation], | ||
| }, | ||
| shadowRadius: shadowLayers[layer].shadowRadius[elevation], | ||
| export type Props = Omit<ViewProps, 'pointerEvents' | 'style'> & |
| * - `overflow: 'hidden'` is not supported with `elevation` as it can clip the shadow. | ||
| * To achieve the same effect, wrap the content in a child View with the overflow style. | ||
| */ | ||
| style?: StyleProp<AnimatedStyle<Omit<ViewStyle, keyof SurfaceVisualProps>>>; |
| theme={theme} | ||
| style={[{ opacity }, styles.content, contentContainerStyle]} | ||
| container | ||
| backgroundColor="transparent" |
f72f11a to
66bded7
Compare
66bded7 to
b61df18
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 59 out of 68 changed files in this pull request and generated 3 comments.
Suppressed comments (3)
src/components/Surface.tsx:258
- Elevation also changes the derived
backgroundColor, but this branch only transitions native elevation. Changing levels on Android makes the surface color jump while the shadow animates; include the background color in the transition as well.
src/components/Surface.tsx:282 - The elevation-derived background changes together with these shadow properties, but it is omitted from the transition. This causes level colors to snap on iOS while the shadow animates; transition
backgroundColortoo.
src/components/Surface.tsx:194 - The central behavior of this refactor—reacting to updates from Reanimated shared/animated styles—is not covered. The new Surface tests only assert static styles, while the prior animation tests in its consumers were removed. Add a test that mutates a shared value and verifies the rendered Surface style updates so metadata-preserving behavior cannot regress.
|
|
||
| transitionStyle = { | ||
| ...transitionStyle, | ||
| transitionProperty: 'boxShadow', |
| elevation={0} | ||
| testID={testID} | ||
| style={{ pointerEvents }} | ||
| onLayout={onLayout} |
| - `Banner` | ||
| - `Button` | ||
| - `Card` | ||
| - `Chip` |
| * Changes Card shadow and background on iOS and Android. | ||
| */ | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5; |
There was a problem hiding this comment.
this could reuse the Elevation type from theme/types/elevation.ts instead of repeating the 0 | 1 | 2 | 3 | 4 | 5 literal union.
| backgroundColor={selected ? selectedBackgroundColor : backgroundColor} | ||
| borderRadius={borderRadius} | ||
| style={[styles.container, styles.md3Container, { borderColor }, style]} | ||
| elevation={elevationStyle} |
There was a problem hiding this comment.
elevationStyle is just an alias for elevation, it's not deriving any style, Surface already turns the elevation level into shadow styles internally. The intermediate const can be dropped.
| elevation={elevationStyle} | |
| elevation={elevation} |
| * Changes Banner shadow and background on iOS and Android. | ||
| */ | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5; |
There was a problem hiding this comment.
this could reuse the Elevation type from theme/types/elevation.ts instead of repeating the 0 | 1 | 2 | 3 | 4 | 5 literal union.
| * Changes Searchbar shadow and background on iOS and Android. | ||
| */ | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5; |
There was a problem hiding this comment.
this could reuse the Elevation type from theme/types/elevation.ts instead of repeating the 0 | 1 | 2 | 3 | 4 | 5 literal union.
| * Changes Snackbar shadow and background on iOS and Android. | ||
| */ | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; | ||
| elevation?: 0 | 1 | 2 | 3 | 4 | 5; |
There was a problem hiding this comment.
this could reuse the Elevation type from theme/types/elevation.ts instead of repeating the 0 | 1 | 2 | 3 | 4 | 5 literal union.
| style={[ | ||
| styles.menuContent, | ||
| Boolean(scrollableMenuHeight) && styles.fill, | ||
| contentStyle, |
There was a problem hiding this comment.
contentStyle is applied to this inner, non-clipping Animated.View instead of to the Surface above it (whose radius is now fixed via the borderRadius prop at line 695). This silently breaks corner-radius overrides — e.g. <Menu contentStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0 }}> no longer squares off the menu's corners, since this inner view has no radius/clipping of its own to apply it to.
Worth noting: Menu isn't listed in docs/6.x/docs/guides/migration.md's "Styles" section alongside the other 7 components whose style/contentStyle intentionally stopped configuring background/radius.
| contentContainerStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>; | ||
| contentContainerStyle?: StyleProp< | ||
| AnimatedStyle<Omit<ViewStyle, 'backgroundColor' | 'borderRadius'>> | ||
| >; |
There was a problem hiding this comment.
This only omits backgroundColor/borderRadius:
But Surface's own style type (Surface.tsx:96) excludes the entire SurfaceVisualProps key set — all 12 per-corner radius keys plus borderCurve. So contentContainerStyle={{ borderTopLeftRadius: 16, borderBottomLeftRadius: 0 }} type-checks fine (Modal has no per-corner prop equivalent), but the value is silently dropped since Surface applies its own visualStyle after style.
| style={[ | ||
| styles.shadowMenuContainer, | ||
| { pointerEvents }, | ||
| shadowMenuContainerStyle, |
There was a problem hiding this comment.
borderRadius is passed to Surface twice: once via the dedicated prop (borderRadius={shadowMenuContainerStyle.borderRadius}, line 695) and again inside shadowMenuContainerStyle, which is also in the style array (line 699).
| justifyContent: 'space-between', | ||
| margin: 8, | ||
| borderRadius: 4, | ||
| minHeight: 48, |
There was a problem hiding this comment.
styles.container and styles.contentContainer both declare minHeight: 48 — leftover duplication from splitting the original single container style. Consider hoisting to one shared constant so a future height tweak can't update one and miss the other.
Motivation
This reworks the
Surfacecomponent to work with Reanimated + updates components that useSurface.Previously, since the component rendered outer and inner views, it flattened styles and picked where to apply what. Style flattening adds challenges when using Reanimated, as Reanimated relies on metadata from
useAnimatedStylefor UI-thread updates.After trying a couple of approaches, I arrived at this approach:
ViewforSurfaceso it can accept both position and layout related stylesViewfor second shadow layer, but instead of wrappingchildrenin it (which adds more challenges), we absolutely position it behind the contentTest plan
Appbar
Android
Before
After
iOS
Before
After
Web
Before
After
Badge
Android
Before
After
iOS
Before
After
Web
Before
After
Banner
Android
Before
banner-android-before.mp4
After
banner-android-after.mp4
iOS
Before
banner-ios-before.mp4
After
banner-ios-after.mp4
Web
Before
banner-web-before.mp4
After
banner-web-after.mp4
Bottom Navigation
Android
Before
bottom-navigation-android-before.mp4
After
bottom-navigation-android-after.mp4
iOS
Before
bottom-navigation-ios-before.mp4
After
bottom-navigation-ios-after.mp4
Web
Before
bottom-navigation-web-before.mp4
After
bottom-navigation-web-after.mp4
Button
Android
Before
After
iOS
Before
After
Web
Before
After
Card
Android
Before
After
iOS
Before
After
Web
Before
After
Chip
Android
Before
After
iOS
Before
After
Web
Before
After
Dialog
Android
Before
dialog-android-before.mp4
After
dialog-android-after.mp4
iOS
Before
dialog-ios-before.mp4
After
dialog-ios-after.mp4
Web
Before
dialog-web-before.mp4
After
dialog-web-after.mp4
FAB
Android
Before
fab-android-before.mp4
After
fab-android-after.mp4
iOS
Before
fab-ios-before.mp4
After
fab-ios-after.mp4
Web
Before
fab-web-before.mp4
After
fab-web-after.mp4
Icon Button
Android
Before
After
iOS
Before
After
Web
Before
After
Menu
Android
Before
menu-android-before.mp4
After
menu-android-after.mp4
iOS
Before
menu-ios-before.mp4
After
menu-ios-after.mp4
Web
Before
menu-web-before.mp4
After
menu-web-after.mp4
Modal
Android
Before
modal-android-before.mp4
After
modal-android-after.mp4
iOS
Before
modal-ios-before.mp4
After
modal-ios-after.mp4
Web
Before
modal-web-before.mp4
After
modal-web-after.mp4
Searchbar
Android
Before
After
iOS
Before
After
Web
Before
After
Snackbar
Android
Before
snackbar-android-before.mp4
After
snackbar-android-after.mp4
iOS
Before
snackbar-ios-before.mp4
After
snackbar-ios-after.mp4
Web
Before
snackbar-web-before.mp4
After
snackbar-web-after.mp4
Surface
Android
Before
surface-android-before.mp4
After
surface-android-after.mp4
iOS
Before
surface-ios-before.mp4
After
surface-ios-after.mp4
Web
Before
surface-web-before.mp4
After
surface-web-after.mp4
Toggle Button
Android
Before
After
iOS
Before
After
Web
Before
After