feat(CSS): support animating backgroundImage gradients - #10193
tshmieldev merged 22 commits into
Conversation
…tion Adds proper support for animating the backgroundImage style prop (and its experimental_backgroundImage alias) in CSS animations, CSS transitions and useAnimatedStyle, which was previously stripped from all animated styles (backgroundImage: false in the style config). JS side: - New processBackgroundImage value processor that normalizes both the CSS string syntax (linear-gradient(...)/radial-gradient(...)) and the object syntax into the processed structure React Native's backgroundImage prop parsing expects, mirroring RN's processBackgroundImage (colors run through processColor so interpolation works) - backgroundImage and experimental_backgroundImage wired into STYLE_PROPERTIES_CONFIG and registered as separately interpolated nested properties so each gradient layer animates independently - Web: processBackgroundImageWeb serializes the object syntax back to a CSS gradient string (strings pass through unchanged) Native side: - New CSSLinearGradient/CSSRadialGradient CSS value types that interpolate gradients smoothly when they are compatible (same direction kind, shape, units and number of color stops): colors, stop positions, angles, sizes and positions all animate; incompatible gradients fall back to the standard discrete flip - Registered in the interpolator registry for both prop names Also adds unit tests (processor, web serializer, keyframes normalization), an example screen in the common app and a supported-properties docs entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p2PjxNunbHriH5853qssK
CSSValueVariant and SimpleValueInterpolator define their member functions in .cpp files with explicit instantiation lists, so the new <CSSLinearGradient, CSSRadialGradient> combination used by the backgroundImage interpolator must be instantiated there as well - otherwise the native build fails with undefined symbol link errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p2PjxNunbHriH5853qssK
The radial gradient CSS string parser consumed the token following a
single explicit size while looking for a second size value and dropped
it when it wasn't a length. When that token was 'at', the whole position
clause was swallowed and its values were re-parsed as a new size
(e.g. 'circle 50% at 25% 25%' produced size 25%/25% with the default
center position). Push the token back so the loop re-processes it.
This bug exists in React Native's own processBackgroundImage as well -
the parser here now handles the '<size> at <position>' combination
correctly.
Also:
- reject gradient strings with trailing characters after the closing
parenthesis instead of silently accepting them
- use valid px circle sizes in the "Moving highlight" example
('circle 50%' is rejected by browsers, so it would break on web)
- document the RN 0.87 / experimental_backgroundImage naming and the
smooth vs discrete interpolation rules in supported-properties
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p2PjxNunbHriH5853qssK
Ports the two upstream react-native processBackgroundImage changes (react/react-native#57873 and react/react-native#57874) into the backgroundImage processor to keep both parsers in sync: - #57873 (position dropped after an explicit size) was already fixed here; align the code comment with upstream and use a px-sized circle in the regression test - #57874: reject a percentage radius for circle radial gradients (explicit 'circle 50%' and the inferred circle from a single '50%' size). Per the CSS spec a circle radius must be a <length>; percentages remain valid for ellipse sizes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p2PjxNunbHriH5853qssK
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change adds native parsing and interpolation for linear and radial CSS gradients used by Priority: ➖ Normal Change: Feature · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Gradient background-image support is merge-ready; the web serializer preserves native circle sizing for explicit numeric dimensions. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Caution CodeRabbit couldn't update its existing comment. The review summary may be out of date. Error details |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/common-app/src/apps/css/examples/animations/screens/animatedProperties/base/appearance/BackgroundImage.tsx`:
- Around line 1-5: In the BackgroundImage animation examples, replace the legacy
experimental_backgroundImage keyframe properties with backgroundImage across all
eight keyframes and remove the file-wide camelcase ESLint disable. Only retain a
narrowly scoped disable if one example intentionally continues covering the
legacy alias.
In
`@packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/complex/CSSBackgroundImage.cpp`:
- Around line 72-81: Replace the fixed six-decimal std::to_string formatting
with a shared compact formatNumber helper in the anonymous namespace, adding the
required iomanip and sstream includes. Use formatNumber in
GradientLengthPercentage::toDynamic() and toString(), and update the angle
formatting around the existing line-240 serialization to use it as well,
preserving percent and CSS unit suffixes.
In
`@packages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/complex/CSSBackgroundImage.h`:
- Around line 6-10: Add a direct <ostream> include to CSSBackgroundImage.h
alongside the standard library headers so the std::ostream declarations used by
its debug stream operators are self-contained, without relying on folly/json.h
transitive includes.
In
`@packages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpp`:
- Around line 210-211: The backgroundImage and experimental_backgroundImage
entries in InterpolatorRegistry must not use CSSLinearGradient() as the default
padding value. Replace it with a transparent non-gradient default, or enforce
matching layer counts so an empty colorStops gradient is never serialized or
sent to React Native.
In
`@packages/react-native-reanimated/src/common/style/processors/__tests__/backgroundImage.test.ts`:
- Around line 336-354: The radial-gradient object test permits an invalid
percentage radius for a circular shape. Update the validation handling in
backgroundImage.ts for the object path, then change this test’s shape to ellipse
so the custom percentage/number radii remain valid, or classify the case as
invalid if that matches the intended validation rules.
- Around line 214-226: Update getPositionFromCSSValue to reject malformed length
values that parseFloat converts to NaN, then extend the invalid-input test.each
cases with linear-gradient(red xpx, blue) and radial-gradient(xpx, red, blue),
ensuring both throw through processBackgroundImage.
In
`@packages/react-native-reanimated/src/common/style/processors/backgroundImage.ts`:
- Around line 771-794: The object-based radial gradient handling in
processBackgroundImageObjects must reuse CSS-path validation: validate size.x,
size.y, and position as non-negative lengths or percentages, apply the circle
percentage-radius restriction from the CSS path, and report invalid positions
through ERROR_MESSAGES.invalidGradientPosition. In
packages/react-native-reanimated/src/common/style/processors/__tests__/backgroundImage.test.ts:336-354,
update the custom-values case to use an ellipse; in :356-372, add a circle with
a percentage radius to the invalid inputs.
- Around line 156-165: Update getPositionFromCSSValue to validate the numeric
portion of both px and percentage values before returning them; reject malformed
inputs such as “xpx” and “a%” by returning null, while preserving valid pixel
numbers and percentage strings.
- Around line 617-624: Update the radial-gradient prelude parsing loop around
hasShapeSizeOrPositionString so an unrecognized token after a recognized shape,
size, or position token throws instead of breaking and discarding the prelude.
Also reject multiple lengths when the shape is circle, allowing only a single
circle radius; preserve the existing color-stop detection for an unrecognized
first token.
In
`@packages/react-native-reanimated/src/common/web/style/processors/backgroundImage.ts`:
- Around line 52-55: Update the color-stop processing around processColor in the
background-image processor to avoid String(color) fallbacks. When
processColor(color) is not a string, skip that stop or throw the established
Reanimated error; preserve the existing formatted output only for valid string
colors.
- Around line 45-51: Update the color-stop serialization in the background-image
processor to pass numeric entries from positions through the existing
maybeAddSuffix helper before joining them, while preserving existing units and
nonnumeric values. Apply this to both transition-hint and regular color-stop
output paths.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: dbd4b119-a81c-4063-b355-9efac67e07cb
📒 Files selected for processing (21)
apps/common-app/src/apps/css/examples/animations/routes/properties/base.tsapps/common-app/src/apps/css/examples/animations/screens/animatedProperties/base/appearance/BackgroundImage.tsxapps/common-app/src/apps/css/examples/animations/screens/animatedProperties/base/appearance/index.tsdocs/docs-reanimated/docs/guides/supported-properties.mdxpackages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpppackages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSValueVariant.cpppackages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/complex/CSSBackgroundImage.cpppackages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/complex/CSSBackgroundImage.hpackages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.cpppackages/react-native-reanimated/src/common/style/config.tspackages/react-native-reanimated/src/common/style/processors/__tests__/backgroundImage.test.tspackages/react-native-reanimated/src/common/style/processors/backgroundImage.tspackages/react-native-reanimated/src/common/style/processors/colors.tspackages/react-native-reanimated/src/common/style/processors/index.tspackages/react-native-reanimated/src/common/style/registry.tspackages/react-native-reanimated/src/common/web/style/config.tspackages/react-native-reanimated/src/common/web/style/processors/__tests__/backgroundImage.test.tspackages/react-native-reanimated/src/common/web/style/processors/backgroundImage.tspackages/react-native-reanimated/src/common/web/style/processors/index.tspackages/react-native-reanimated/src/css/native/__tests__/registry.test.tspackages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts
|
Codex review finding (P2): explicit circle sizes serialize to invalid CSS on web I’m Codex reviewing this PR. In {
type: 'radial-gradient',
shape: 'circle',
size: { x: 100, y: 100 },
colorStops: [{ color: 'red' }, { color: 'blue' }],
}becomes The CSS radial-gradient grammar permits a single non-negative Relevant code: CSS specification: https://drafts.csswg.org/css-images/#radial-gradients Please branch on |
…n web The web serializer emitted both axis values for circle sizes (e.g. 'circle 100px 100px'), but the CSS radial-gradient grammar only allows a single non-negative length as an explicit circle radius - two values describe an ellipse and a percentage radius is invalid, so browsers rejected the whole declaration. Serialize an equal-axis length circle size as a single radius and throw for combinations that cannot be represented as a valid circle (unequal axes or percentages). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p2PjxNunbHriH5853qssK
# Conflicts: # packages/react-native-reanimated/src/common/web/style/config.ts
…dimage-support-as2wrl Takes the tree of @tshmieldev/bgimg-css, which is based on current main where the backgroundImage processor from software-mansion#10486 already lives.
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 84550791-9219-424c-b475-3b404102acef
📒 Files selected for processing (22)
apps/common-app/runtime-tests/reanimated/suites.tsapps/common-app/runtime-tests/reanimated/tests/css/backgroundImage.test.tsxapps/common-app/src/apps/css/examples/animations/routes/properties/base.tsapps/common-app/src/apps/css/examples/animations/screens/animatedProperties/base/appearance/others/BackgroundImage.tsxapps/common-app/src/apps/css/examples/animations/screens/animatedProperties/base/appearance/others/index.tsdocs/docs-reanimated/docs/guides/supported-properties.mdxpackages/react-native-reanimated/Common/cpp/reanimated/CSS/InterpolatorRegistry.cpppackages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/CSSValueVariant.cpppackages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/complex/CSSGradient.cpppackages/react-native-reanimated/Common/cpp/reanimated/CSS/common/values/complex/CSSGradient.hpackages/react-native-reanimated/Common/cpp/reanimated/CSS/interpolation/values/SimpleValueInterpolator.cpppackages/react-native-reanimated/changelog/bgimg-css.feature.mdpackages/react-native-reanimated/src/PropsRegistryGarbageCollector.tspackages/react-native-reanimated/src/common/style/config.tspackages/react-native-reanimated/src/common/style/processors/__tests__/backgroundImage.test.tspackages/react-native-reanimated/src/common/style/processors/backgroundImage.tspackages/react-native-reanimated/src/common/style/registry.tspackages/react-native-reanimated/src/common/web/style/config.tspackages/react-native-reanimated/src/common/web/style/processors/__tests__/backgroundImage.test.tspackages/react-native-reanimated/src/common/web/style/processors/backgroundImage.tspackages/react-native-reanimated/src/css/native/__tests__/registry.test.tspackages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/react-native-reanimated/src/common/style/registry.ts
- docs/docs-reanimated/docs/guides/supported-properties.mdx
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
…ircle size on web
|
Thanks @Titozzz! |
Note
This pull request was authored by AI on behalf of @Titozzz and @tshmieldev.
Summary
Adds
backgroundImage(linear and radial gradients) to CSS animations and CSS transitions, on native and on the Web. Fixes #8297.The JS processor from the original version of this PR landed separately in #10486 (it also enabled gradients in
useAnimatedStyle). This PR now contains the CSS engine part on top of currentmain, merged from #10664, which it supersedes.Native
CSSGradient(Common/cpp/reanimated/CSS/common/values/complex/CSSGradient.{h,cpp}) registered asarray({value<CSSGradient>(CSSGradient())}), one interpolator per layer, likeboxShadow. It reads and emits the exact shape produced byprocessBackgroundImage, so React Native parses the animated value with its existingbackgroundImageparser on both platforms. Gradient type, radial shape and radial size keyword are enums; a length is a{value, isPercent}struct that serializes back to a number or a"N%"string.backgroundImage(registered for React Native 0.87 and newer, where the name exists) andexperimental_backgroundImage(registered unconditionally; React Native 0.87 and 0.88 still read it as an alias). Both are separately interpolated nested properties, so keyframes are split per layer index.red, blue→red, blue, limeanimates instead of snapping;nullin both keyframes staysnullin the output so React Native resolves it;FALLBACK_INTERPOLATION_THRESHOLD): linear ↔ radial, keyword ↔ angle direction, different keywords, shape mismatch, size keyword ↔ explicit size,px↔%, transition hint ↔ color stop at the same index.Web
processBackgroundImageWebturns the object form into a CSSlinear-gradient()/radial-gradient()string (strings pass through), with the same defaults as the native processor. A circle with an explicit size is emitted ascircle max(x, y)px(what iOS renders); a circle with a percentage size, which CSS does not allow, is emitted asellipse x y.experimental_backgroundImageis emitted underbackground-image.background-imageas a discrete animation type, so on the Web the gradient switches at the midpoint between keyframes; the docs say so.Related
Unsupported type for radius property: Nullonce per color stop without a position on everybackgroundImageupdate (also for plain React Native, and at frame rate for any animated gradient). Fix in React Native: fix(android): skip null gradient color stop positions instead of warning react/react-native#58636.Test plan
Example screen:
apps/common-app→ CSS → Animations → Animated properties → Others → Background Image (Linear / Radial / Layers tabs). Checked on the Android emulator (Pixel 9 Pro, API 37), the iPhone 17 simulator, and Expo web.Runtime tests (new suite
css/backgroundImage.test.tsx, 8 cases; it pins a paused animation at a known progress with a negativeanimationDelayand reads the parsed native prop back withgetViewProp):10/10 on Android after every native change in this PR.
Jest:
yarn workspace react-native-reanimated jest backgroundImage registry keyframes(native processor incl. validation cases, web processor incl. circle and default cases, nested properties registry, keyframes normalization).yarn workspace react-native-reanimated type:check,type:check:legacy-rn-types,lint:js;clang-formatv19,scripts/validate-common.shandvalidate-includeson the C++.Minimal snippet:
Expected: the gradient rotates a full turn while its colors blend, on both platforms. On React Native 0.86 use
experimental_backgroundImage.Changelog
Unpublishedsection of each changed package'sCHANGELOG.md, or this PR does not changereact-native-reanimatedorreact-native-worklets.