Skip to content

Interpolate gradient color stops outside the 0–100% range - #59774

Open
intergalacticspacehighway wants to merge 5 commits into
mainfrom
gradient-out-of-range-color-stops
Open

intergalacticspacehighway wants to merge 5 commits into
mainfrom
gradient-out-of-range-color-stops

Conversation

@intergalacticspacehighway

@intergalacticspacehighway intergalacticspacehighway commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

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:

Before and after on iOS and Android, compared with Chrome

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.
@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 Nov 3, 2026
@intergalacticspacehighway
intergalacticspacehighway marked this pull request as ready for review November 3, 2026 09:14
@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 Nov 3, 2026
@intergalacticspacehighway intergalacticspacehighway added the p: Expo Partner: Expo label Nov 3, 2026
@meta-codesync

meta-codesync Bot commented Nov 3, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D129320610.

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),
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

@intergalacticspacehighway intergalacticspacehighway Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! Pushed in a795489. The behaviour now matches chrome.

@javache
javache enabled auto-merge (squash) November 4, 2026 10:07

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. p: Expo Partner: Expo 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.

2 participants