Animating SVG
Reanimated can animate react-native-svg components - both their geometry (cx, r, d, points, ...) and their appearance (fill, stroke, opacity, ...). You can drive them with inline props, the useAnimatedProps hook, or the modern, declarative CSS animations and CSS transitions.
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 feature flag. Animating SVG with useAnimatedProps or inline shared values doesn't require it.
Setup
Install react-native-svg. SVG components aren't built into Reanimated, so wrap the ones you animate with createAnimatedComponent:
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 - pass a shared value straight to the prop:
<AnimatedCircle r={r} />. useAnimatedProps- compute the props in a worklet and hand the result to theanimatedPropsprop.- CSS - keep the SVG props as plain values and add
animationNameortransitionProperty(with their settings) toanimatedProps.
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 doesn't re-render - each r.value change updates the r prop directly - so the CSS transition never runs:
// 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 through useAnimatedProps:
const r = useSharedValue(20);
useEffect(() => {
const easing = Easing.bezier(0.25, 0.1, 0.25, 1); // CSS `ease`
r.value = withRepeat(withTiming(50, { duration: 1000, easing }), -1, true);
}, []);
const animatedProps = useAnimatedProps(() => ({
r: r.value,
}));
return (
<View style={styles.container}>
<Svg style={styles.svg}>
{/* Pass the animated props to your animated component */}
<AnimatedCircle
cx="50%"
cy="50%"
fill="#b58df1"
animatedProps={animatedProps}
/>
</Svg>
</View>
);
With CSS animations
The same animation expressed as a CSS keyframe animation:
<AnimatedCircle
cx="50%"
cy="50%"
r={20}
fill="#b58df1"
animatedProps={{
animationName: {
from: { r: 20 },
to: { r: 50 },
},
animationDuration: '1s',
animationIterationCount: 'infinite',
animationDirection: 'alternate',
animationTimingFunction: 'ease',
}}
/>
With CSS transitions
A CSS transition 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:
<AnimatedCircle
cx="50%"
cy="50%"
fill="#b58df1"
r={grown ? 50 : 20}
animatedProps={{
transitionProperty: 'r',
transitionDuration: 300,
transitionTimingFunction: 'ease',
}}
/>
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:
// 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 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 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:
const CIRCLE =
'M50 10 C72 10 90 28 90 50 C90 72 72 90 50 90 C28 90 10 72 10 50 C10 28 28 10 50 10 Z';
const STAR =
'M50 10 C50 22 78 50 90 50 C78 50 50 78 50 90 C50 78 22 50 10 50 C22 50 50 22 50 10 Z';
export default function App() {
return (
<View style={styles.container}>
<Svg height={160} width="100%" viewBox="0 0 100 100">
<AnimatedPath
fill="#b58df1"
d={CIRCLE}
animatedProps={{
animationName: {
from: { d: CIRCLE },
to: { d: STAR },
},
animationDuration: '2s',
animationIterationCount: 'infinite',
animationDirection: 'alternate',
animationTimingFunction: 'ease-in-out',
}}
/>
</Svg>
</View>
);
}
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; shape components also animate their geometry. Properties that aren't listed aren't supported by CSS - use 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 | 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 |
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. 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:
<Svg height={SIZE} width={SIZE}>
<Defs>
<AnimatedRadialGradient
id="grad"
cx="50%"
cy="50%"
r="65%"
animatedProps={{
// Morph from a 2-stop "sun" to a 4-stop "sunset"
animationName: {
from: {
gradient: [
{ offset: '0%', color: '#fdbb2d' },
{ offset: '100%', color: '#22c1c3' },
],
},
to: {
gradient: [
{ offset: '0%', color: '#fdbb2d' },
{ offset: '30%', color: '#b21f1f' },
{ offset: '60%', color: '#1a2a6c' },
{ offset: '100%', color: '#000000' },
],
},
},
animationDuration: '2s',
animationIterationCount: 'infinite',
animationDirection: 'alternate',
animationTimingFunction: 'ease-in-out',
}}
/>
</Defs>
<Circle
cx={SIZE / 2}
cy={SIZE / 2}
r={SIZE / 2 - 10}
fill="url(#grad)"
/>
</Svg>
A few caveats:
- You can't mix
<Stop>children and thegradientprop - if both are present, thegradientprop wins. gradientUnitsis a stepped property (it jumps).- Gradient coordinates can't mix percentage strings and
0-1fractions in one animation (see Units). - Gradients animate on native platforms only (not the web).
RadialGradientfx/fyanimate on iOS only -react-native-svgcan'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 for those.