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..51d5fd67d0f9 100644 --- a/docs/docs-reanimated/docs/css-transitions/transition-property.mdx +++ b/docs/docs-reanimated/docs/css-transitions/transition-property.mdx @@ -86,6 +86,8 @@ import TransitionPropertySrc from '!!raw-loader!@site/src/examples/css-transitio - 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..08873a47f6f5 --- /dev/null +++ b/docs/docs-reanimated/docs/guides/animating-svg.mdx @@ -0,0 +1,201 @@ +--- +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 plain values and add `animationName` or `transitionProperty` (with their settings) to `animatedProps`. + +There's one catch with **CSS transitions**: a CSS transition only runs when the prop changes **between renders**. A plain value does that, so the CSS transition runs. An inline [shared value](/docs/fundamentals/glossary#shared-value) doesn't re-render - each `r.value` change updates the `r` prop directly - so the CSS transition never runs: + +```tsx +// Plain value: it changes on re-render, which triggers the CSS transition +; + +// Shared value: r.value changes don't trigger the CSS transition - they update the r prop directly +const r = useSharedValue(20); +; +``` + +## 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. You can pass the prop inline or through `animatedProps`; changing it from either place starts the transition, and if both set it, `animatedProps` wins: + +```tsx +// inline - changing r triggers the transition + + +// through animatedProps - same effect + +``` + +A [shared value](/docs/fundamentals/glossary#shared-value) is the exception: updating `r.value` doesn't re-render, so it never starts a transition. To animate a prop from a shared value, pass it inline (`r={r}`) or via [`useAnimatedProps`](/docs/core/useAnimatedProps) instead. + +## 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; 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) morph freely between any shapes on iOS and Android. On Web, only `Path` morphs: its `d` interpolates between paths that share the same command structure, and mismatched structures snap. `Polygon` and `Polyline` `points` don't animate on Web at all, because `react-native-svg` renders them as native ``/`` elements whose `points` is not a CSS property. + +### 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'; + +export const gradientVideo = { + light: '/recordings/examples/svg_radial_gradient_light.mp4', + dark: '/recordings/examples/svg_radial_gradient_dark.mp4', +}; + + + +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 - `Polygon`/`Polyline` `points`, `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..d81e04544fff 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", "react-native-svg": "15.15.4", "react-native-web": "0.21.2", - "react-native-worklets": "0.9.1", + "react-native-worklets": "0.10.0", "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..07be74d65293 --- /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 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..efd8a8df6465 --- /dev/null +++ b/docs/docs-reanimated/src/components/SideBySideExample/styles.module.css @@ -0,0 +1,64 @@ +.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; + min-width: 340px; + display: flex; +} + +.row > .preview > *, +.row > .code > * { + flex: 1; + min-width: 0; + min-height: 0; + margin: 0; +} + +.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 { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + border: 1px solid var(--swm-border); + background-color: var(--swm-off-background); +} + +.video { + width: 100%; + max-height: 100%; + aspect-ratio: 1 / 1; + 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..a6dd8de8475b --- /dev/null +++ b/docs/docs-reanimated/src/examples/svg/SvgCssAnimation.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +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..bf589f8e92ce --- /dev/null +++ b/docs/docs-reanimated/src/examples/svg/SvgCssTransition.tsx @@ -0,0 +1,43 @@ +import React, { 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 ( + + + + +