From 3ab90fb2e3b6c3827051855b50261a842355c1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 17 Jun 2026 08:07:58 +0200 Subject: [PATCH 01/20] Add guide for animating react-native-svg Covers useAnimatedProps, inline shared values, CSS animations and transitions, path morphing, and gradients. Gradients animate on native only, so that section uses iOS recordings shown next to their code. Adds a SideBySideExample docs component, runnable examples, and cross-links from related pages, and bumps the docs reanimated/worklets to a nightly so the web examples animate. --- .../docs/core/useAnimatedProps.mdx | 1 + .../docs/css-animations/animation-name.mdx | 1 + .../css-transitions/transition-property.mdx | 1 + .../animating-styles-and-props.mdx | 2 + .../docs/guides/animating-svg.mdx | 204 ++++++++++++++++++ .../docs/guides/supported-properties.mdx | 2 + docs/docs-reanimated/package.json | 4 +- .../components/SideBySideExample/index.tsx | 80 +++++++ .../SideBySideExample/styles.module.css | 88 ++++++++ .../src/examples/svg/SvgCssAnimation.tsx | 42 ++++ .../src/examples/svg/SvgCssTransition.tsx | 43 ++++ .../src/examples/svg/SvgGradient.tsx | 61 ++++++ .../src/examples/svg/SvgPathMorph.tsx | 46 ++++ .../src/examples/svg/SvgUseAnimatedProps.tsx | 53 +++++ .../examples/svg_radial_gradient_dark.mp4 | Bin 0 -> 935720 bytes .../examples/svg_radial_gradient_light.mp4 | Bin 0 -> 823816 bytes yarn.lock | 43 +++- 17 files changed, 667 insertions(+), 4 deletions(-) create mode 100644 docs/docs-reanimated/docs/guides/animating-svg.mdx create mode 100644 docs/docs-reanimated/src/components/SideBySideExample/index.tsx create mode 100644 docs/docs-reanimated/src/components/SideBySideExample/styles.module.css create mode 100644 docs/docs-reanimated/src/examples/svg/SvgCssAnimation.tsx create mode 100644 docs/docs-reanimated/src/examples/svg/SvgCssTransition.tsx create mode 100644 docs/docs-reanimated/src/examples/svg/SvgGradient.tsx create mode 100644 docs/docs-reanimated/src/examples/svg/SvgPathMorph.tsx create mode 100644 docs/docs-reanimated/src/examples/svg/SvgUseAnimatedProps.tsx create mode 100644 docs/docs-reanimated/static/recordings/examples/svg_radial_gradient_dark.mp4 create mode 100644 docs/docs-reanimated/static/recordings/examples/svg_radial_gradient_light.mp4 diff --git a/docs/docs-reanimated/docs/core/useAnimatedProps.mdx b/docs/docs-reanimated/docs/core/useAnimatedProps.mdx index 6ea28263d798..f5ab91380a6c 100644 --- a/docs/docs-reanimated/docs/core/useAnimatedProps.mdx +++ b/docs/docs-reanimated/docs/core/useAnimatedProps.mdx @@ -121,6 +121,7 @@ import AnimatingPropsSrc from '!!raw-loader!@site/src/examples/AnimatingProps'; - You can share animated props between components to avoid code duplication. - We recommend to create adapters outside of the component's body to avoid unnecessary recalculations. +- For animating [`react-native-svg`](https://github.com/software-mansion/react-native-svg) components, see [Animating SVG](/docs/guides/animating-svg). ## Platform compatibility diff --git a/docs/docs-reanimated/docs/css-animations/animation-name.mdx b/docs/docs-reanimated/docs/css-animations/animation-name.mdx index 6f46e9a366b1..f51f6fe134dd 100644 --- a/docs/docs-reanimated/docs/css-animations/animation-name.mdx +++ b/docs/docs-reanimated/docs/css-animations/animation-name.mdx @@ -92,6 +92,7 @@ import AnimationNameSrc from '!!raw-loader!@site/src/examples/css-animations/Ani - At minimum 1 keyframe is required to create an animation. Reanimated will take the current state of the element as the first keyframe. - If multiple animations target the same property, the animation later in the array will override changes from the previous one. +- You can animate [`react-native-svg`](https://github.com/software-mansion/react-native-svg) components too - see [Animating SVG](/docs/guides/animating-svg). ## Platform compatibility diff --git a/docs/docs-reanimated/docs/css-transitions/transition-property.mdx b/docs/docs-reanimated/docs/css-transitions/transition-property.mdx index ac86f571d5a3..7346b5aae332 100644 --- a/docs/docs-reanimated/docs/css-transitions/transition-property.mdx +++ b/docs/docs-reanimated/docs/css-transitions/transition-property.mdx @@ -85,6 +85,7 @@ import TransitionPropertySrc from '!!raw-loader!@site/src/examples/css-transitio - Discrete style properties (like `flexDirection`, `justifyContent`) can't be smoothly animated using the `transitionProperty` property. For example, you can't animate smoothly from `alignItems: start` to `alignItems: center`. You can use [Layout Animations](/docs/layout-animations/layout-transitions) to animate discrete style properties instead. - We discourage the use of `all` property as it can lead to performance issues. +- You can also transition [`react-native-svg`](https://github.com/software-mansion/react-native-svg) props - see [Animating SVG](/docs/guides/animating-svg). ## Platform compatibility diff --git a/docs/docs-reanimated/docs/fundamentals/animating-styles-and-props.mdx b/docs/docs-reanimated/docs/fundamentals/animating-styles-and-props.mdx index 52e33ab682eb..b6c14f220399 100644 --- a/docs/docs-reanimated/docs/fundamentals/animating-styles-and-props.mdx +++ b/docs/docs-reanimated/docs/fundamentals/animating-styles-and-props.mdx @@ -121,6 +121,8 @@ Check out the full example below: +For a complete guide to animating `react-native-svg` - including CSS animations and transitions and which props are supported - see [Animating SVG](/docs/guides/animating-svg). + ## Summary In this section, we went through the differences between animating styles and props and how to use `useAnimatedStyle` and `useAnimatedProps`. To sum up: diff --git a/docs/docs-reanimated/docs/guides/animating-svg.mdx b/docs/docs-reanimated/docs/guides/animating-svg.mdx new file mode 100644 index 000000000000..f91d2980c5e0 --- /dev/null +++ b/docs/docs-reanimated/docs/guides/animating-svg.mdx @@ -0,0 +1,204 @@ +--- +id: animating-svg +title: Animating SVG +sidebar_label: Animating SVG +--- + +Reanimated can animate [`react-native-svg`](https://github.com/software-mansion/react-native-svg) components - both their geometry (`cx`, `r`, `d`, `points`, ...) and their appearance (`fill`, `stroke`, `opacity`, ...). You can drive them with [inline props](/docs/fundamentals/animating-styles-and-props#animating-props), the [`useAnimatedProps`](/docs/core/useAnimatedProps) hook, or the modern, declarative [CSS animations](/docs/css-animations/animation-name) and [CSS transitions](/docs/css-transitions/transition-property). + +:::info + +CSS animations and transitions for SVG are an experimental feature, enabled by default from Reanimated 4.4. You can opt out with the [`EXPERIMENTAL_CSS_ANIMATIONS_FOR_SVG_COMPONENTS`](/docs/guides/feature-flags#experimental_css_animations_for_svg_components) feature flag. Animating SVG with [`useAnimatedProps`](/docs/core/useAnimatedProps) or inline [shared values](/docs/fundamentals/glossary#shared-value) doesn't require it. + +::: + +## Setup + +Install [`react-native-svg`](https://github.com/software-mansion/react-native-svg). SVG components aren't built into Reanimated, so wrap the ones you animate with [`createAnimatedComponent`](/docs/core/createAnimatedComponent): + +```tsx +import Animated from 'react-native-reanimated'; +import { Circle } from 'react-native-svg'; + +const AnimatedCircle = Animated.createAnimatedComponent(Circle); +``` + +## Animating SVG values + +SVG attributes like `cx`, `r`, `d`, and `fill` are **component props**, not React Native `style` keys, so they animate through props rather than `style`. There are three ways to drive them: + +- **[Inline](/docs/fundamentals/animating-styles-and-props#animating-props)** - pass a [shared value](/docs/fundamentals/glossary#shared-value) straight to the prop: ``. +- **[`useAnimatedProps`](/docs/core/useAnimatedProps)** - compute the props in a worklet and hand the result to the `animatedProps` prop. +- **CSS** - keep the SVG props as normal props and add `animationName` or `transitionProperty` (with their settings) to `animatedProps`. + +CSS transitions can be triggered by a prop passed inline or through `animatedProps` - changing it from either place starts the transition. If a prop is set in both, `animatedProps` takes precedence: + +```tsx +// r passed inline - changing it triggers the transition + + +// r passed through animatedProps - same effect + +``` + +## With useAnimatedProps + +Drive an attribute with a [shared value](/docs/fundamentals/glossary#shared-value) through [`useAnimatedProps`](/docs/core/useAnimatedProps): + +import SideBySideExample from '@site/src/components/SideBySideExample'; +import SvgUseAnimatedProps from '@site/src/examples/svg/SvgUseAnimatedProps'; +import SvgUseAnimatedPropsSrc from '!!raw-loader!@site/src/examples/svg/SvgUseAnimatedProps'; + + + +## With CSS animations + +The same animation expressed as a [CSS keyframe animation](/docs/css-animations/animation-name): + +import SvgCssAnimation from '@site/src/examples/svg/SvgCssAnimation'; +import SvgCssAnimationSrc from '!!raw-loader!@site/src/examples/svg/SvgCssAnimation'; + + + +## With CSS transitions + +A [CSS transition](/docs/css-transitions/transition-property) runs whenever a transitioned value changes between renders - including a plain value from `useState` or props, with no shared value involved. Just change the prop: + +import SvgCssTransition from '@site/src/examples/svg/SvgCssTransition'; +import SvgCssTransitionSrc from '!!raw-loader!@site/src/examples/svg/SvgCssTransition'; + + + +The first render never animates (there is no previous value); each later change of `r` transitions from the old value to the new one. + +## Morphing paths + +A `Path` morphs between two shapes when both use the same sequence of commands. Because `d` maps to a real CSS property, this runs on the web as well: + +import SvgPathMorph from '@site/src/examples/svg/SvgPathMorph'; +import SvgPathMorphSrc from '!!raw-loader!@site/src/examples/svg/SvgPathMorph'; + + + +## Supported components and properties + +The tables below cover what **CSS animations and transitions** can animate, and on which platforms. Every component animates the [common appearance properties](#common-appearance-properties); shape components also animate their [geometry](#geometry-by-component). Properties that aren't listed aren't supported by CSS - use [`useAnimatedProps`](/docs/core/useAnimatedProps) for those, since it can drive any animatable prop the component accepts. + +### Common appearance properties + +Every `react-native-svg` component supports these props, so they can animate on any of them: + +
+ +| Property | Android | iOS | Web | +| ------------------ | ------- | ------ | ------ | +| `color` | | | | +| `fill` | | | | +| `fillOpacity` | | | | +| `fillRule` | | | | +| `stroke` | | | | +| `strokeWidth` | | | | +| `strokeOpacity` | | | | +| `strokeDasharray` | | | | +| `strokeDashoffset` | | | | +| `strokeLinecap` | | | | +| `strokeLinejoin` | | | | +| `vectorEffect` | | | | +| `opacity` | | | | +| `pointerEvents` | | | | +| `clipPath` | | | | +| `clipRule` | | | | +| `mask` | | | | +| `filter` | | | | +| `marker` | | | | + +
+ +### Geometry by component + +These components animate their geometry on iOS and Android. The **Web** column shows whether that geometry animates there too. + +| Component | Geometry props | Web | +| --------------------- | ------------------------------------------------------------------- | --- | +| `Circle` | `cx`, `cy`, `r` | ✅ | +| `Ellipse` | `cx`, `cy`, `rx`, `ry` | ✅ | +| `Rect` | `x`, `y`, `width`, `height`, `rx`, `ry` | ✅ | +| `Image` | `x`, `y`, `width`, `height` | ✅ | +| `Path` | `d` | ✅¹ | +| `Polygon`, `Polyline` | `points` | ✅¹ | +| `Line` | `x1`, `y1`, `x2`, `y2` | ❌ | +| `Text` | `x`, `y`, `dx`, `dy`, `rotate` | ❌ | +| `Pattern` | `x`, `y`, `width`, `height`, `patternUnits`, `patternContentUnits` | ❌ | +| `LinearGradient` | `x1`, `y1`, `x2`, `y2`, `gradient`, `gradientUnits` | ❌ | +| `RadialGradient` | `cx`, `cy`, `r`, `rx`, `ry`, `fx`, `fy`, `gradient`, `gradientUnits` | ❌ | + +¹ On Web, `Path` `d` only morphs between paths with matching command structure, and `points` only between equal point counts; mismatches snap. iOS and Android morph freely. + +The remaining components - `G`, `Use`, `Symbol`, `Defs`, `ClipPath`, `Mask`, `Marker`, `TSpan`, `TextPath`, and `ForeignObject` - have no animatable geometry; they animate only the [common appearance properties](#common-appearance-properties). On Web, only `G` is supported among them. + +`Pattern` `x`/`y` are iOS only - `react-native-svg` doesn't support them on Android, even outside animations. `Text` `x`, `y`, `dx`, `dy`, and `rotate` also accept per-glyph arrays. + +## Remarks + +### Morphing paths and points + +`d` (Path) and `points` (Polygon/Polyline) interpolate smoothly when the start and end share the same topology - the same path command structure, or the same number of points. On iOS and Android, mismatched shapes still morph; on Web they snap to the target, because CSS only interpolates `d`/`points` between matching structures. + +### Stepped properties + +Some properties step between discrete values rather than interpolating: `strokeLinecap`, `strokeLinejoin`, `fillRule`, `vectorEffect`, `gradientUnits`, and `patternUnits`. This matches native SVG and CSS behavior. + +### Units + +Geometry props (`cx`, `r`, `x`, `width`, ...) accept plain numbers or percentage strings (`'50%'`), and a single animation can mix the two: an absolute value and a percentage are resolved to the same unit first, so `r` animates smoothly even from `10` to `'50%'`. + +Gradient and pattern **coordinates** are the exception: there, a percentage string (`'50%'`) and a `0`-`1` fraction don't interpolate into each other, so a single animation has to use one or the other. + +### Gradients + +`react-native-svg` defines gradient stops with `` children, which can't be animated. To animate them, Reanimated adds a `gradient` prop - an array of `{ offset, color, opacity }` stops that replaces the children. Each stop's `offset`, `color`, and `opacity` can animate, and even the number of stops can differ between `from` and `to`. The gradient's geometry animates too: `x1`/`y1`/`x2`/`y2` for `LinearGradient`, and `cx`/`cy`/`r`/`fx`/`fy`/`rx`/`ry` for `RadialGradient`. + +Here, a `RadialGradient` morphs from a two-stop "sun" to a four-stop "sunset". Gradients don't animate on the web, so this is recorded on iOS: + +import SvgGradientSrc from '!!raw-loader!@site/src/examples/svg/SvgGradient'; + + + +A few caveats: + +- You can't mix `` children and the `gradient` prop - if both are present, the `gradient` prop wins. +- `gradientUnits` is a [stepped property](#stepped-properties) (it jumps). +- Gradient coordinates can't mix percentage strings and `0`-`1` fractions in one animation (see [Units](#units)). +- Gradients animate on **native platforms only** (not the web). `RadialGradient` `fx`/`fy` animate on iOS only - `react-native-svg` can't apply the focal point on Android. + +### Web + +On Web, Reanimated drives SVG through CSS, so an attribute animates only if it maps to a real CSS property. Attributes that have no CSS equivalent - `Line` endpoints, `Text`/`Pattern`/gradient coordinates, and gradient stops - can't animate via CSS on Web; use [`useAnimatedProps`](/docs/core/useAnimatedProps) for those. diff --git a/docs/docs-reanimated/docs/guides/supported-properties.mdx b/docs/docs-reanimated/docs/guides/supported-properties.mdx index 9417202aee6f..980918593452 100644 --- a/docs/docs-reanimated/docs/guides/supported-properties.mdx +++ b/docs/docs-reanimated/docs/guides/supported-properties.mdx @@ -6,6 +6,8 @@ id: supported-properties Not all CSS properties are available and animatable in React Native. The following table describes which style properties can be animated on which platform. +For animating `react-native-svg` components, see [Animating SVG](/docs/guides/animating-svg). +
| Property | Android | iOS | Web | | ----------------------- | ------- | ------ | ------ | diff --git a/docs/docs-reanimated/package.json b/docs/docs-reanimated/package.json index 544f8d11e479..54316a623d4c 100644 --- a/docs/docs-reanimated/package.json +++ b/docs/docs-reanimated/package.json @@ -58,10 +58,10 @@ "react-dom": "19.1.1", "react-native": "0.83.0", "react-native-gesture-handler": "2.28.0", - "react-native-reanimated": "4.4.1", + "react-native-reanimated": "4.5.0-nightly-20260614-41c1d1a75", "react-native-svg": "15.15.4", "react-native-web": "0.21.2", - "react-native-worklets": "0.9.1", + "react-native-worklets": "0.10.0-nightly-20260614-41c1d1a75", "source-map": "0.7.4", "source-map-loader": "4.0.1", "typescript": "5.9.3", diff --git a/docs/docs-reanimated/src/components/SideBySideExample/index.tsx b/docs/docs-reanimated/src/components/SideBySideExample/index.tsx new file mode 100644 index 000000000000..4021928150e3 --- /dev/null +++ b/docs/docs-reanimated/src/components/SideBySideExample/index.tsx @@ -0,0 +1,80 @@ +import BrowserOnly from '@docusaurus/BrowserOnly'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import CollapsibleCode from '@site/src/components/CollapsibleCode'; +import ReducedMotionWarning from '@site/src/components/ReducedMotionWarning'; +import clsx from 'clsx'; +import type React from 'react'; +import { useReducedMotion } from 'react-native-reanimated'; + +import styles from './styles.module.css'; + +type BaseProps = { + /** Raw source of the example, shown in the collapsible code block. */ + src: string; + /** Lines initially shown in the code block (0-indexed, inclusive). */ + showLines: number[]; +}; + +type PreviewProps = { + component: React.FC; + video?: never; +}; + +type VideoProps = { + component?: never; + video: { light: string; dark: string }; +}; + +type Props = BaseProps & (PreviewProps | VideoProps); + +export default function SideBySideExample({ + src, + showLines, + component, + video, +}: Props) { + const Component = component; + const prefersReducedMotion = useReducedMotion(); + + return ( +
+
+ {video ? ( +
+ + +
+ ) : ( +
+ Loading...
}> + {() => ( + <> + {prefersReducedMotion && } + {Component ? : null} + + )} + +
+ )} +
+
+ +
+
+ ); +} diff --git a/docs/docs-reanimated/src/components/SideBySideExample/styles.module.css b/docs/docs-reanimated/src/components/SideBySideExample/styles.module.css new file mode 100644 index 000000000000..d172595be1e8 --- /dev/null +++ b/docs/docs-reanimated/src/components/SideBySideExample/styles.module.css @@ -0,0 +1,88 @@ +/* Preview and code sit side by side, and wrap to a stacked layout (preview above + code, each full width) as soon as the row can no longer hold both min-widths + plus the gap: 240 + 340 + 24 = 604px. */ +.row { + display: flex; + flex-wrap: wrap; + gap: 24px; + align-items: stretch; + margin: 16px 0; +} + +.preview { + flex: 1 1 0; + min-width: 240px; + display: flex; +} + +.code { + flex: 2 1 0; + /* Wrap before the code column gets too narrow to read. */ + min-width: 340px; + display: flex; +} + +/* Make the preview and the code fill their columns so the two sit at the same + height; the child selector raises specificity to drop the inner cards' margins. */ +.row > .preview > *, +.row > .code > * { + flex: 1; + min-width: 0; + min-height: 0; + margin: 0; +} + +/* Live (web-animatable) preview, rendered directly without InteractiveExample's + chrome. Mirrors the video card so both preview kinds look the same. */ +.liveCard { + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--swm-border); + background-color: var(--swm-off-background); +} + +.loading { + margin: auto; + padding: 24px; +} + +.videoCard { + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--swm-border); + background-color: var(--swm-off-background); +} + +/* Hide the iOS Dynamic Island at the top of the recording. The clip's backdrop + is the same theme color, so this strip blends in seamlessly. */ +.videoCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 8%; + z-index: 1; + background-color: var(--swm-off-background); +} + +/* Fill the full-height card with the recording (cover), cropping only the empty + side margins. The clip stays anchored at the top, so the masking strip keeps + the Dynamic Island covered. */ +.video { + flex: 1; + width: 100%; + min-height: 0; + object-fit: cover; +} + +[data-theme='light'] .videoDark { + display: none; +} + +[data-theme='dark'] .videoLight { + display: none; +} diff --git a/docs/docs-reanimated/src/examples/svg/SvgCssAnimation.tsx b/docs/docs-reanimated/src/examples/svg/SvgCssAnimation.tsx new file mode 100644 index 000000000000..42d89a13048d --- /dev/null +++ b/docs/docs-reanimated/src/examples/svg/SvgCssAnimation.tsx @@ -0,0 +1,42 @@ +import { StyleSheet, View } from 'react-native'; +import Animated from 'react-native-reanimated'; +import { Circle, Svg } from 'react-native-svg'; + +const AnimatedCircle = Animated.createAnimatedComponent(Circle); + +export default function App() { + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + height: '100%', + alignItems: 'center', + justifyContent: 'center', + }, + svg: { height: 100, width: '100%' }, +}); diff --git a/docs/docs-reanimated/src/examples/svg/SvgCssTransition.tsx b/docs/docs-reanimated/src/examples/svg/SvgCssTransition.tsx new file mode 100644 index 000000000000..063f6fbb6f04 --- /dev/null +++ b/docs/docs-reanimated/src/examples/svg/SvgCssTransition.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { Button, StyleSheet, View } from 'react-native'; +import Animated from 'react-native-reanimated'; +import { Circle, Svg } from 'react-native-svg'; + +const AnimatedCircle = Animated.createAnimatedComponent(Circle); + +export default function App() { + const [grown, setGrown] = useState(false); + + return ( + + + + +