Repository navigation
Interpolate gradient color stops outside the 0–100% range - #59774
Open
intergalacticspacehighway wants to merge 5 commits into
Open
intergalacticspacehighway wants to merge 5 commits into
intergalacticspacehighway wants to merge 5 commits into
Conversation
CSS lets color stops sit before 0% or after 100%. They still shape the colors inside the gradient through interpolation, but are never painted directly. React Native clamped such stops to the range on iOS, and Android's gradient shaders pin them, so `linear-gradient(red -50%, blue 150%)` rendered as plain `red → blue`, while browsers start at rgb(191, 0, 64). Replace out-of-range stops with stops at the range boundaries, colored as the gradient would be at that point (premultiplied, as CSS and Android's shaders interpolate). Radial gradients only clip negative stops: like Blink, they grow the radius to fit stops past the ending shape, because that area stays visible. Also like Blink, length stops on a zero-length gradient line resolve to 0, so the radius stays finite. Android also dropped negative stop positions while parsing linear and radial gradients, treating them as unpositioned. Allow them.
Mirror the Android ColorStopTest cases for RCTGradientUtils clipColorStops and getFixedColorStops in the React-RCTFabric test spec. Credit the WPT tests that some cases come from, in both the iOS and Kotlin tests.
intergalacticspacehighway
marked this pull request as ready for review
November 3, 2026 09:14
javache
approved these changes
Nov 3, 2026
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D129320610. |
javache
reviewed
Nov 3, 2026
Comment on lines
+194
to
+208
| private fun colorAt( | ||
| before: ProcessedColorStop, | ||
| after: ProcessedColorStop, | ||
| position: Float, | ||
| ): Int? { | ||
| val beforeColor = before.color ?: return after.color | ||
| val afterColor = after.color ?: return beforeColor | ||
| val beforePosition = before.position ?: return beforeColor | ||
| val afterPosition = after.position ?: return afterColor | ||
| return interpolateColor( | ||
| beforeColor, | ||
| afterColor, | ||
| (position - beforePosition) / (afterPosition - beforePosition), | ||
| ) | ||
| } |
Contributor
There was a problem hiding this comment.
Crash: IllegalArgumentException: Cannot round NaN value. thrown during draw when a color stop position is NaN or infinite.
The new interpolateColor calls .roundToInt() on (alpha * 255f) and on each channel. Kotlin's Float.roundToInt() throws on NaN. Nothing stops a NaN weight from getting here:
Collaborator
Author
There was a problem hiding this comment.
Thanks! Pushed in a795489. The behaviour now matches chrome.
javache
enabled auto-merge (squash)
November 4, 2026 10:07
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
Found while reviewing #58143 (comment): gradient color stops outside 0–100% are clamped to the range instead of interpolated. For example,
linear-gradient(to right, red -50%, yellow 25%, blue 150%)starts at pure red instead of orange.This replaces out-of-range stops with boundary stops of the interpolated color, as browsers do. Radial gradients grow the radius for stops past 100%, like Blink.
Changelog:
[GENERAL] [FIXED] - Interpolate gradient color stops outside the 0–100% range instead of clamping them
Test Plan:
Added "Color stops outside 0–100%" examples to the Linear Gradient and Radial Gradient pages in RNTester. Before/after on iOS (iPhone 17 Pro simulator) and Android (emulator), compared with Chrome:
./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.uimanager.style.*":ColorStopTestpasses (24 tests). Some cases mirror the WPT tests normalization-linear.html, normalization-linear-2.html, normalization-radial.html, normalization-radial-2.html.React/Tests/Fabric/RCTGradientUtilsTests.mm(16 tests) passes locally through theReact-RCTFabric-Unit-Testsscheme. CI does not build this test spec.