-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
docs: Guide for animating react-native-svg with CSS and animatedProps
#9682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MatiPl01
wants to merge
19
commits into
main
Choose a base branch
from
@matipl01/svg-docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
3ab90fb
Add guide for animating react-native-svg
MatiPl01 e695b24
Add TODO to bump reanimated/worklets back to stable after release
MatiPl01 0940c9f
Sync docs-worklets to the reanimated/worklets nightly
MatiPl01 95a8e76
Remove nightly-pin TODO comments from docs package.json files
MatiPl01 68412da
Format new docs with format:md
MatiPl01 567796d
Move gradient video paths to a const so format:md is idempotent
MatiPl01 547580d
Cap the gradient recording height so it doesn't balloon on code expand
MatiPl01 e0d0a97
Fit the gradient recording within its card (contain) instead of cappi…
MatiPl01 bb26fb7
Crop the gradient recording to its centered gradient (square card)
MatiPl01 472587d
Match the video card to the code height and center the cropped clip i…
MatiPl01 a856b33
Strip CSS module comments and note that shared values don't drive CSS…
MatiPl01 97f3812
Potential fix for pull request finding
MatiPl01 c33b75e
Potential fix for pull request finding
MatiPl01 7c742f9
Apply suggestions from code review
MatiPl01 cdf9b51
Contrast imperative shared values with declarative CSS, with an inlin…
MatiPl01 ac32146
Reformat SvgGradient and realign showLines after the added React imports
MatiPl01 6b9d75b
Reframe the overview comparison around CSS transitions (plain vs shar…
MatiPl01 9791e9f
Correct Polygon/Polyline web support in the SVG animation guide
MatiPl01 a34135e
Simplify the Polygon/Polyline web note in the SVG guide
MatiPl01 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: `<AnimatedCircle r={r} />`. | ||
| - **[`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 | ||
| <AnimatedCircle | ||
| r={grown ? 50 : 20} | ||
| animatedProps={{ transitionProperty: 'r', transitionDuration: 300 }} | ||
| />; | ||
|
|
||
| // Shared value: r.value changes don't trigger the CSS transition - they update the r prop directly | ||
| const r = useSharedValue(20); | ||
| <AnimatedCircle | ||
| r={r} | ||
| animatedProps={{ transitionProperty: 'r', transitionDuration: 300 }} | ||
| />; | ||
| ``` | ||
|
|
||
| ## 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'; | ||
|
|
||
| <SideBySideExample src={SvgUseAnimatedPropsSrc} component={SvgUseAnimatedProps} showLines={[14, 41]}/> | ||
|
|
||
| ## 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'; | ||
|
|
||
| <SideBySideExample src={SvgCssAnimationSrc} component={SvgCssAnimation} showLines={[11, 28]}/> | ||
|
|
||
| ## 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'; | ||
|
|
||
| <SideBySideExample src={SvgCssTransitionSrc} component={SvgCssTransition} showLines={[13, 26]}/> | ||
|
|
||
| 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 | ||
| <AnimatedCircle | ||
| r={grown ? 50 : 20} | ||
| animatedProps={{ transitionProperty: 'r', transitionDuration: 300 }} | ||
| /> | ||
|
|
||
| // through animatedProps - same effect | ||
| <AnimatedCircle | ||
| animatedProps={{ r: grown ? 50 : 20, transitionProperty: 'r', transitionDuration: 300 }} | ||
| /> | ||
| ``` | ||
|
|
||
| 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'; | ||
|
|
||
| <SideBySideExample src={SvgPathMorphSrc} component={SvgPathMorph} showLines={[7, 37]}/> | ||
|
|
||
| ## 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: | ||
|
|
||
| <div className="compatibility fixed"> | ||
| | Property | Android | iOS | Web | | ||
| | ------------------ | ------- | ------ | ------ | | ||
| | `color` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `fill` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `fillOpacity` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `fillRule` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `stroke` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `strokeWidth` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `strokeOpacity` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `strokeDasharray` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `strokeDashoffset` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `strokeLinecap` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `strokeLinejoin` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `vectorEffect` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `opacity` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `pointerEvents` | <Yes/> | <Yes/> | <Yes/> | | ||
| | `clipPath` | <Yes/> | <Yes/> | <No/> | | ||
| | `clipRule` | <Yes/> | <Yes/> | <No/> | | ||
| | `mask` | <Yes/> | <Yes/> | <No/> | | ||
| | `filter` | <Yes/> | <Yes/> | <No/> | | ||
| | `marker` | <Yes/> | <Yes/> | <No/> | | ||
| </div> | ||
|
|
||
| ### 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 `<polygon>`/`<polyline>` 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 `<Stop>` 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', | ||
| }; | ||
|
|
||
| <SideBySideExample src={SvgGradientSrc} showLines={[12, 52]} video={gradientVideo}/> | ||
|
|
||
| A few caveats: | ||
|
|
||
| - You can't mix `<Stop>` 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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
|
Comment on lines
+61
to
+64
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Temporary, will be replaced with a valid latest stable version after the release.
Comment on lines
59
to
+64
|
||
| "source-map": "0.7.4", | ||
| "source-map-loader": "4.0.1", | ||
| "typescript": "5.9.3", | ||
|
|
||
80 changes: 80 additions & 0 deletions
80
docs/docs-reanimated/src/components/SideBySideExample/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className={styles.row}> | ||
| <div className={styles.preview}> | ||
| {video ? ( | ||
| <div className={styles.videoCard}> | ||
| <video | ||
| className={clsx(styles.video, styles.videoLight)} | ||
| autoPlay | ||
| loop | ||
| muted | ||
| playsInline> | ||
| <source src={useBaseUrl(video.light)} type="video/mp4" /> | ||
| </video> | ||
| <video | ||
| className={clsx(styles.video, styles.videoDark)} | ||
| autoPlay | ||
| loop | ||
| muted | ||
| playsInline> | ||
| <source src={useBaseUrl(video.dark)} type="video/mp4" /> | ||
| </video> | ||
| </div> | ||
| ) : ( | ||
| <div className={styles.liveCard}> | ||
| <BrowserOnly | ||
| fallback={<div className={styles.loading}>Loading...</div>}> | ||
| {() => ( | ||
| <> | ||
| {prefersReducedMotion && <ReducedMotionWarning />} | ||
| {Component ? <Component /> : null} | ||
| </> | ||
| )} | ||
| </BrowserOnly> | ||
| </div> | ||
| )} | ||
| </div> | ||
| <div className={styles.code}> | ||
| <CollapsibleCode src={src} showLines={showLines} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.