From adffc115b0f289d148f33f96d148116f00bc5bb4 Mon Sep 17 00:00:00 2001 From: Thibault Malbranche Date: Mon, 10 Aug 2026 12:17:04 +0200 Subject: [PATCH 1/2] Fix radial-gradient position being dropped after an explicit size In processBackgroundImage, the explicit-size branch shifts the next token to look for a second size value and discards it when it is not a length or percentage. When that token is 'at', the whole position clause is lost: the position defaults back to center and the position values are then re-parsed as a new size, silently overriding the declared one. radial-gradient(circle 100px at 25% 75%, red, blue) previously parsed as size {x: '25%', y: '75%'} with position {top: '50%', left: '50%'}; it now parses as size {x: 100, y: 100} with position {left: '25%', top: '75%'}. The existing test for this syntax only used 'at center', which is indistinguishable from the default position, so the bug was invisible. Co-Authored-By: Claude Fable 5 --- .../__tests__/processBackgroundImage-itest.js | 16 ++++++++++++++++ .../StyleSheet/processBackgroundImage.js | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js index 08a0ca0b9700..2614d334dd9a 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js @@ -999,6 +999,22 @@ describe('processBackgroundImage', () => { expect(result[0].type).toEqual('radial-gradient'); }); + it('should handle radial gradient with explicit size and position', () => { + const input = 'radial-gradient(circle 100px at 25% 75%, red, blue)'; + const result = processBackgroundImage(input); + expect(result[0].shape).toEqual('circle'); + expect(result[0].size).toEqual({x: 100, y: 100}); + expect(result[0].position).toEqual({left: '25%', top: '75%'}); + }); + + it('should handle radial gradient with two explicit sizes and position', () => { + const input = 'radial-gradient(50px 100px at left bottom, red, blue)'; + const result = processBackgroundImage(input); + expect(result[0].shape).toEqual('ellipse'); + expect(result[0].size).toEqual({x: 50, y: 100}); + expect(result[0].position).toEqual({left: '0%', top: '100%'}); + }); + // 1. position syntax: [ left | center | right | top | bottom | ] it('should handle radial gradient position length syntax', () => { const input = 'radial-gradient(circle at 20px, red, blue)'; diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index f733c96c9645..421884005bbe 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -348,6 +348,11 @@ function parseRadialGradientCSSString( size = {x: sizeX, y: sizeY}; } else { hasExplicitSingleSize = true; + // The token after the size is not a second size value (e.g. 'at' or a + // shape keyword). Put it back so the loop can process it, otherwise + // the position would be silently dropped and its values re-parsed as + // a new size. + firstPartTokens.unshift(token); } } else if (tokenTrimmed === 'at') { let top: string | number; From ea0613c439cfe6eddcdb8c071abb0b1680cf7c5f Mon Sep 17 00:00:00 2001 From: Thibault Malbranche Date: Mon, 10 Aug 2026 12:23:34 +0200 Subject: [PATCH 2/2] Reject percentage radius for circle radial gradients Per the CSS spec (css-images-3, ), a circle's explicit radius must be a - percentages are only valid for ellipses. Browsers reject the whole declaration for values such as radial-gradient(circle 50%, red, blue), while React Native accepted them and rendered an arbitrary interpretation (max of both resolved axes), so the same style silently diverged between native and web. processBackgroundImage now returns no gradient for a circle (explicit or inferred from a single size) whose size is a percentage, matching web behavior. Ellipses with percentage sizes are unaffected. Co-Authored-By: Claude Fable 5 --- .../__tests__/processBackgroundImage-itest.js | 17 +++++++++++++++++ .../StyleSheet/processBackgroundImage.js | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js index 2614d334dd9a..5dc05646b27e 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js @@ -986,6 +986,23 @@ describe('processBackgroundImage', () => { expect(result[0].shape).toEqual('ellipse'); }); + it('should reject a circle with a percentage radius', () => { + const input = 'radial-gradient(circle 50%, red, blue)'; + expect(processBackgroundImage(input)).toEqual([]); + }); + + it('should reject an inferred circle with a percentage radius', () => { + const input = 'radial-gradient(50%, red, blue)'; + expect(processBackgroundImage(input)).toEqual([]); + }); + + it('should allow percentage sizes for ellipses', () => { + const input = 'radial-gradient(50% 20%, red, blue)'; + const result = processBackgroundImage(input); + expect(result[0].shape).toEqual('ellipse'); + expect(result[0].size).toEqual({x: '50%', y: '20%'}); + }); + it('should handle radial gradient with explicit shape with size', () => { const input = 'radial-gradient(circle 100px at center, red, blue 80%)'; const result = processBackgroundImage(input); diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index 421884005bbe..23c0b20cc525 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -567,6 +567,15 @@ function parseRadialGradientCSSString( // If a single size is explicitly set and the shape is an ellipse, return null and do not apply any gradient. Same as web. return null; } + + if ( + shape === 'circle' && + typeof size === 'object' && + (typeof size.x === 'string' || typeof size.y === 'string') + ) { + // A circle radius must be a . Percentages are only valid for ellipses, so return null and do not apply any gradient. Same as web. + return null; + } } const colorStops = parseColorStopsCSSString(remainingParts);