Skip to content

Match functional colors against the whole string in normalize-colors - #59103

Open
YevheniiKotyrlo wants to merge 1 commit into
react:mainfrom
YevheniiKotyrlo:fix/normalize-colors-anchored-functional-matchers
Open

YevheniiKotyrlo wants to merge 1 commit into
react:mainfrom
YevheniiKotyrlo:fix/normalize-colors-anchored-functional-matchers

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

@react-native/normalize-colors anchors its hex matchers but not rgb, rgba, hsl, hsla and hwb, so a string that only contains one of those calls normalizes to that color: normalizeColor('xxrgb(1, 2, 3)yy') is 0x010203ff (#58495).

Animated pays for it. AnimatedInterpolation treats an output range that normalizes as a color as a color, so a JS-driven interpolation of a box shadow such as inset 0 0 0 1px rgba(124, 58, 237, 1) returns the bare rgba(…), which processBoxShadow parses to no shadow at all.

This anchors the five functional matchers, as the hex ones already are. It is the change #58496 proposed before its author closed it.

Changelog:

[GENERAL] [FIXED] - An Animated interpolation of a string that contains an rgb(), hsl() or hwb() color, such as a box shadow, interpolates the whole string instead of collapsing to the color

Test Plan:

normalizeColor-test.js gains a case for strings that only contain a functional color, which returned 16909311 for 'xxrgb(1, 2, 3)yy' before the change. Interpolation-itest.js gains a box-shadow range, which returned 'rgba(124, 58, 237, 1)' before the change and now answers 'inset 0 0 0 2px rgba(124, 58, 237, 0.75)' halfway.

  • yarn jest packages/normalize-color packages/react-native/Libraries/Animated packages/react-native/Libraries/StyleSheet: 80 passed
  • yarn flow check: no errors
  • eslint and prettier --check on the three files: clean
  • RNTester release builds on a Galaxy S23 (Android 14), before and after: 24 views animating an inset ring through a JS-driven interpolation draw no ring without the change and the ring at its interpolated alpha with it

Under Fantom, this PR's CI run passes Interpolation-itest.js and normalizeColor-itest.js.

normalize-colors anchors its hex matchers but not rgb, rgba, hsl, hsla
and hwb, so a string that only contains one of those calls normalizes
to that color. AnimatedInterpolation reads an output range that
normalizes as a color, so a JS-driven interpolation of a box shadow
such as `inset 0 0 0 1px rgba(124, 58, 237, 1)` returns the bare color,
which processBoxShadow parses to no shadow at all.

Anchor the five functional matchers, as the hex ones already are.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 15, 2026
@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — the interpolated box shadow collapses to its bare rgba(…) color, which parses to no shadow, so the views never draw their ring.

FIXED — the views draw the ring at the interpolated alpha.

before — main at 83eb67a5e7 after — with this PR

RNTester release builds for arm64-v8a from main at 83eb67a5e7 with and without this change, on a Galaxy S23 (Android 14, API 34), 1080×2340 at density 480. The scene animates the boxShadow of 24 views with a JS-driven interpolation from inset 0 0 0 3px rgba(124, 58, 237, 1) to 0.7 alpha and back every second. I took three frames from each build: without the change they are identical and hold no ring pixels; with it the ring blends at alpha 0.90, 0.75 and 0.70.

Device evidence — before / after, recorded

UNFIXED — no frame draws the ring.

FIXED — the ring follows the interpolation, from alpha 1 to 0.7 and back every second.

Left: main at 83eb67a5e7. Right: with this PR. The same builds and phone as the pair above; both 5 s screen recordings start about 2 s after the example opens, and the GIF plays 3 s of each from 1 s in, at 25 frames a second. The phone captured 102 frames a second without the change and 108 with it; on a row across the first five views, none of the 510 frames without the change holds a ring pixel and all 541 with it do. Every frame: before · after.

Scene
function AnimatedInterpolatedBoxShadowExample() {
  const progress = React.useRef(new Animated.Value(0)).current;
  React.useEffect(() => {
    const loop = Animated.loop(
      Animated.timing(progress, {
        toValue: 1,
        duration: 1000,
        easing: Easing.linear,
        useNativeDriver: false,
      }),
    );
    loop.start();
    return () => loop.stop();
  }, [progress]);
  const boxShadow = progress.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: [
      'inset 0 0 0 3px rgba(124, 58, 237, 1)',
      'inset 0 0 0 3px rgba(124, 58, 237, 0.7)',
      'inset 0 0 0 3px rgba(124, 58, 237, 1)',
    ],
  });
  return (
    <View style={{flexDirection: 'row', flexWrap: 'wrap', gap: 12, padding: 8}}>
      {Array.from({length: 24}, (_, index) => (
        <Animated.View
          key={index}
          style={{
            width: 48,
            height: 48,
            borderRadius: 8,
            backgroundColor: '#e5e7eb',
            boxShadow,
          }}
        />
      ))}
    </View>
  );
}

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 18, 2026 22:23
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 18, 2026

This branch has not been deployed

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant