@rootnative/inertia-svg
v0.0.9
Published
Animatable SVG primitives (path morphing, fill/stroke) for @rootnative/inertia, built on react-native-svg.
Maintainers
Readme
@rootnative/inertia-svg
Animatable SVG primitives for @rootnative/inertia, built on react-native-svg.
MotionPath accepts the same initial / animate / transition shape as the core Motion.* primitives, with animatable keys for the path data (d), color paint (fill, stroke), and numeric paint (strokeWidth, opacities, strokeDashoffset). Prebuilt MotionCircle / MotionRect / MotionLine cover the other common shapes, and the createMotionSvgComponent factory behind them wraps any react-native-svg element. Everything is also reachable through the MotionSvg namespace (MotionSvg.Path / .Circle / .Rect / .Line).
Install
pnpm add @rootnative/inertia-svg react-native-svgreact-native-svg works in bare React Native projects as well as Expo.
Peer dependencies: @rootnative/inertia (workspace or installed), react >=19.0.0, react-native >=0.81.0, react-native-reanimated >=4.0.0, react-native-svg >=15.0.0.
Usage
import Svg from 'react-native-svg'
import { MotionPath } from '@rootnative/inertia-svg'
export function Heart({ beating }) {
return (
<Svg viewBox="0 0 100 100" width={120} height={120}>
<MotionPath
d="M 50 30 L 70 12 L 90 30 L 50 88 L 10 30 L 30 12 Z"
fill="#ef4444"
stroke="#991b1b"
strokeWidth={3}
animate={{
d: beating
? 'M 50 28 L 71 10 L 92 28 L 50 92 L 8 28 L 29 10 Z'
: 'M 50 30 L 70 12 L 90 30 L 50 88 L 10 30 L 30 12 Z',
}}
transition={{ type: 'spring', tension: 200, friction: 10 }}
/>
</Svg>
)
}The static d prop sets the visual on first render and locks the command sequence for the component's lifetime. Every target d (via initial or animate) must produce the same command letters in the same order after implicit-repeat expansion. To switch between structurally different shapes, remount with a new key.
Animatable props
| Key | Type | Notes |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------- |
| d | string | Path morph via element-wise scalar interpolation. Source and target must share the same template. |
| fill | string | Color, interpolated by Reanimated's color setter. |
| stroke | string | Color. |
| strokeWidth | number | Numeric. |
| strokeOpacity | number | 0–1. |
| fillOpacity | number | 0–1. |
| opacity | number | 0–1. |
| strokeDashoffset | number | Useful for "draw-in" animations on a dashed stroke. |
Per-property transitions are supported — pass a { [key]: TransitionConfig } shape to transition instead of a single config, e.g.
transition={{
d: { type: 'spring', tension: 160, friction: 14 },
fill: { type: 'timing', duration: 300 },
}}Structural compatibility
✅ M 0 0 L 10 10 Z ↔ M 50 50 L 80 80 Z same template: M L Z
✅ M 0 0 10 10 20 20 ↔ M 0 0 L 30 30 L 40 40 same after implicit-repeat expansion (M → L)
❌ M 0 0 L 10 10 Z ↔ M 0 0 L 10 10 segment count differs
❌ M 0 0 L 10 10 ↔ M 0 0 l 10 10 absolute vs relative are distinct templates
❌ M 0 0 L 10 10 ↔ M 0 0 C 1 1 2 2 3 3 command letters differIn dev, the component throws on template mismatches at mount, when animate.d changes shape, or when the static d prop itself changes shape between renders. In production those errors degrade to a no-op snap so a single bad target doesn't crash the screen.
Path resampling between structurally different shapes (flubber-style) is out of scope by design. For arbitrary shape swaps, remount with key={...}.
Shapes — MotionCircle, MotionRect, MotionLine
Prebuilt geometry primitives with the same initial / animate / transition surface. Each animates its numeric geometry props (cx / cy / r, x / y / width / height / rx / ry, x1 / y1 / x2 / y2), the fill / stroke paints (stroke only for MotionLine), opacities, strokeDashoffset — and strokeDasharray element-wise, with the array length locked at mount (the same rule MotionPath applies to path commands). The canonical consumer is a circular progress ring:
import Svg from 'react-native-svg'
import { MotionCircle } from '@rootnative/inertia-svg'
const CIRCUMFERENCE = 2 * Math.PI * 40
<Svg viewBox="0 0 100 100" width={96} height={96}>
<MotionCircle
cx={50}
cy={50}
r={40}
fill="none"
stroke="#4f46e5"
strokeWidth={8}
strokeDasharray={[CIRCUMFERENCE]}
animate={{ strokeDashoffset: CIRCUMFERENCE * (1 - progress) }}
transition={{ type: 'timing', duration: 250 }}
/>
</Svg>createMotionSvgComponent
The factory behind the prebuilt shapes — wraps any other react-native-svg element:
import { Ellipse } from 'react-native-svg'
import { createMotionSvgComponent } from '@rootnative/inertia-svg'
const MotionEllipse = createMotionSvgComponent(Ellipse, {
animatableProps: ['cx', 'cy', 'rx', 'ry', 'strokeWidth', 'opacity'],
colorProps: ['fill', 'stroke'],
arrayProps: ['strokeDasharray'],
})animatableProps are numeric, colorProps are color strings, arrayProps are numeric arrays (element-wise, length locked at mount). transition accepts named transitions from the nearest <MotionConfig transitions>, top-level and per-property.
Reduced motion
All primitives participate in <MotionConfig reducedMotion> just like the core ones — when the OS reduce-motion setting is on (or you pass reducedMotion="always"), every animated property snaps directly to its target.
What this package doesn't do
- Path resampling between arbitrary shapes.
- Morphing an
Linto aC(or other across-command interpolation). Element-wise scalar interpolation is intentional.
Documentation
- Full docs: https://rootnative.github.io/inertia/docs/svg
- Core library:
@rootnative/inertia
AI agents: this package ships its own copy of the reference — read node_modules/@rootnative/inertia-svg/llms.txt for the full API, no network needed.
License
MIT
