Skip to content

feat(touchable-ripple): MD3 state-layer opacity via android_ripple alpha - #4997

Draft
adrcotfas wants to merge 5 commits into
@adrcotfas/chore_rn86from
@adrcotfas/remove_state_layer_pressed
Draft

adrcotfas wants to merge 5 commits into
@adrcotfas/chore_rn86from
@adrcotfas/remove_state_layer_pressed

Conversation

@adrcotfas

Copy link
Copy Markdown
Collaborator

Motivation

MD3 interaction state layers are an opaque color drawn at a fixed opacity (0.1 pressed, 0.08 hover). Previously the theme shipped a pre-baked stateLayerPressed color (onSurface at 0.1 alpha) because PlatformColor can't be alpha-manipulated in JS, so the opacity had to be baked into the token ahead of time.

React Native 0.86's android_ripple.alpha (react/react-native#56395) removes that limitation: the alpha can now be applied at draw time, even over a PlatformColor. This lets us drop the pre-baked theme color and give TouchableRipple ownership of the state-layer opacity, so any component can pass its own opaque state-layer color and get the correct pressed/hover opacity for free.

This PR does that in three steps:

  1. Remove stateLayerPressed from the theme. TouchableRipple now defaults its ripple to onSurface and applies the pressed opacity itself, so the workaround color is no longer needed.
  2. Rework the TouchableRipple API. The ripple color is treated as opaque and the MD3 pressed opacity is applied automatically (multiplied into the color's own alpha, matching android_ripple.alpha), so a transparent ripple color stays invisible on web too. The public surface is just rippleColor plus the raw background escape hatch. Web and native now share one TouchableRippleCommonProps type, and background is typed as PressableAndroidRippleConfig instead of Object.
  3. Make the FAB ripple variant-aware. Each FAB variant gets a stateLayer role (the container's on-color), resolved following the container and ignoring a contentColor override, and passed to TouchableRipple as rippleColor. The pressed/hover state layer now matches the variant (e.g. primary → onPrimary, tonalPrimary → onPrimaryContainer) instead of always using onSurface.

Test plan

lint, typescript and tests pass

Postponed until #4996 is merged

Bump react-native and the @react-native/* presets to 0.86.0. React stays at 19.2.3 and there are no native template changes between 0.85.3 and 0.86.0.

Bump react-native-reanimated to 4.4.1 and react-native-worklets to 0.9.2 for RN 0.86 support. In the example, bump react-native-gesture-handler to 2.32.0 (RN 0.86 removed the legacy renderer shim that 2.31.x imported) and align the Expo SDK 56 packages to their latest patch versions.
reanimated 4.x and worklets ship "type": "module" builds with extensionless relative imports, which webpack 5 rejects under its fully-specified ESM rule and breaks the docs build. Disable fullySpecified for just those two packages so their .web.js variants resolve.
stateLayerPressed pre-baked onSurface at the pressed opacity because PlatformColor can't be alpha-manipulated in JS. RN 0.86's android_ripple alpha makes that workaround unnecessary, so the field is dropped from the theme. TouchableRipple now defaults its ripple to onSurface and applies the opacity separately.
Replace the rippleAlpha prop with the MD3 pressed opacity applied
automatically, so the public API is just an opaque rippleColor plus
the raw background escape hatch.

- Extract a shared TouchableRippleCommonProps type so web and native
  expose one API, and type background as PressableAndroidRippleConfig
  instead of Object.
- Multiply the opacity into the color's own alpha (matching Android's
  android_ripple.alpha) so a transparent rippleColor stays invisible
  on web; fill the pressed opacity into a custom background only when
  it doesn't set its own alpha.
Add a stateLayer role to each FAB variant (the container's on-color) and resolve it following the container, ignoring a contentColor override. Shell passes it as rippleColor so the pressed and hover state layers match the variant instead of always using onSurface.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Four unresolved moderate findings affect ripple contrast, compatibility, and custom underlay behavior.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates MD3 ripple opacity handling with android_ripple.alpha, removes the precomputed theme token, and adds variant-aware FAB state layers.

Changes:

  • Applies pressed/hover opacity in native and web ripples.
  • Adds FAB variant state-layer roles.
  • Updates ripple types, dependencies, tests, and tooling configuration.

Review findings:

  • Moderate (3 votes): FAB.Menu morphing triggers can use the wrong close-state ripple color.
  • Moderate (3 votes): FAB menu items do not receive the resolved variant state layer.
  • Moderate (2 votes): Older React Native versions lack an opacity fallback while the peer requirement remains unrestricted.
  • Moderate (1 vote): Explicit underlayColor values now receive unintended opacity.
File summaries
File Description
yarn.lock Locks updated dependencies.
src/theme/types/color.ts Removes stateLayerPressed.
src/theme/tokens/sys/color.ts Stops precomputing the state-layer color.
src/components/TouchableRipple/utils.ts Defaults ripple colors to opaque onSurface.
src/components/TouchableRipple/types.ts Adds shared typed ripple props.
src/components/TouchableRipple/TouchableRipple.tsx Applies web opacity handling.
src/components/TouchableRipple/TouchableRipple.native.tsx Configures native ripple alpha.
src/components/FAB/utils.ts Resolves FAB state-layer colors.
src/components/FAB/tokens.ts Adds variant-specific state-layer roles.
src/components/FAB/Shell.tsx Passes state-layer colors to ripples.
src/components/__tests__/TouchableRipple.test.tsx Tests ripple opacity and configuration.
src/components/__tests__/FABUtils.test.tsx Tests state-layer resolution.
src/components/__tests__/__snapshots__/ListSection.test.tsx.snap Updates theme snapshots.
package.json Updates React Native dependencies.
example/package.json Updates example dependencies.
docs/plugins/docusaurus-react-native-plugin.js Adjusts webpack dependency resolution.
Review details

Suppressed comments (1)

src/components/TouchableRipple/TouchableRipple.native.tsx:107

  • This applies the 0.1 opacity to explicit underlayColor values too. Previously a custom underlay was rendered directly, so underlayColor="purple" now becomes a 10%-opacity layer even though this separate override is documented as the underlay color. Apply the opacity only to the default underlay, or explicitly document this breaking behavior.
                  opacity: state.opacity.pressed,
  • Files reviewed: 15/16 changed files
  • Comments generated: 3
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

<TouchableRipple
borderless
background={background}
rippleColor={colors.stateLayer}
export type ResolvedColors = {
container: ColorValue;
content: ColorValue;
stateLayer: ColorValue;
color: calculatedRippleColor,
borderless,
foreground: useForeground,
alpha: state.opacity.pressed,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants