Declarative animations for React Native, on iOS, Android, and the web.
An animation is a prop on a component: no worklets, no shared values, no useAnimatedStyle.
Open the live demo Β· Try it Β· Quick start Β· Documentation Β· LLM docs
Status:
0.0.26. The declarative core, layout, and shared elements are complete and tested. Before1.0.0, a minor release can still change the API. The changelog names each breaking change.
| Feature | What it does |
|---|---|
| Animations as props | Set initial, animate, exit, and transition on a Motion.* component. You write no worklet and no shared value. |
| Types for each primitive | Motion.View takes ViewStyle keys, Motion.Text takes TextStyle keys, and Motion.Image takes ImageStyle keys. |
| Springs with designer names | A spring takes tension, friction, and mass. Timing, decay, and no-animation transitions are also available. |
| Sequences and keyframes | An array in animate runs one step after the other. Each step can have its own transition. |
| Variants | Name the states once, then write animate="open". The common case needs no hook. |
One gesture prop |
Each primitive animates its pressed, focused, focusVisible, and hovered states. A primitive with no gesture prop has no extra cost. |
| Presence | <Presence> plays the exit animation before a child unmounts. An exiting child does not take taps. |
| Layout and shared elements | layout animates a change of size or position. layoutId moves one element between two places in the tree. |
| Stagger | <Stagger> delays each child by its position in the list. |
| Reduced motion | <MotionConfig> follows the reduce-motion setting of the OS from end to end. It also holds your named transitions. |
| Hooks when you need them | useMotionValue, useSpring, useTransform, useScroll, useInView, useHideOnScroll, and more. |
| Adapters | Drag, swipe, and pan with Gesture Handler, animated linear gradients, and SVG path morphing. Each adapter is a separate package. |
| Fast re-renders | The JS thread resolves a transition once per change. A render with the same animate makes no new UI-thread closure. |
| In a browser | On a phone |
|---|---|
|
rootnative.github.io/inertia/example
One screen for each primitive, transition, and hook. You do not need an install or an account. |
Clone this repository, then runpnpm install and pnpm run example.
Scan the QR code in the terminal with Expo Go. |
npx expo install @rootnative/inertia react-native-reanimated react-native-workletsnpx expo install picks the Reanimated and Worklets versions that match your
Expo SDK. Expo adds the worklets Babel plugin for you. A bare React Native app
adds react-native-worklets/plugin as the last Babel plugin.
import { Motion } from '@rootnative/inertia'
export function FadeIn() {
return (
<Motion.View
initial={{ opacity: 0, translateY: 20 }}
animate={{ opacity: 1, translateY: 0 }}
transition={{
opacity: { type: 'timing', duration: 200 },
translateY: { type: 'spring', tension: 180, friction: 12 },
}}
/>
)
}To ship only one primitive, import it from its subpath:
import { MotionView } from '@rootnative/inertia/view'.
Install an adapter only when you need it:
npx expo install @rootnative/inertia-gestures react-native-gesture-handler # useDrag, useSwipe, usePan
npx expo install @rootnative/inertia-gradients expo-linear-gradient # MotionLinearGradient
npx expo install @rootnative/inertia-svg react-native-svg # MotionPath and SVG shapesImportant
Each release line supports one Expo SDK band. An app on SDK 57 needs
0.0.19. See Expo SDK compatibility.
Tip
For the web, run npx expo install react-dom react-native-web. A bundler
without Metro (Vite, webpack, Electron) must run the worklets plugin on
node_modules too. The
installation guide
shows the config.
type |
Public config keys | Maps to |
|---|---|---|
'spring' (default) |
tension, friction, mass, velocity |
withSpring (converted from spring physics) |
'timing' |
duration, easing, delay |
withTiming |
'decay' |
velocity, deceleration, clamp |
withDecay |
'no-animation' |
β | direct assignment, no interpolation |
Each transition also takes delay and repeat (a count, 'infinite', or
{ count, alternate }). A map for each property can also hold config keys.
Those keys are the default for each key that the map does not name:
{ type: 'spring', tension: 120, opacity: { type: 'timing' } } springs each
key except opacity.
The animatable properties are opacity, transforms, size, border radius, shadows, and colours. The full list is in Transitions.
flowchart LR
subgraph js ["JS thread"]
direction LR
props["Motion.View props<br/>initial, animate, transition"] --> resolver["Resolver<br/>once per change"]
end
subgraph ui ["UI thread"]
direction LR
worklet["Memoized worklet<br/>animated style"]
end
resolver -->|withSpring, withTiming, withDecay| worklet
worklet --> view["Native view<br/>or DOM node"]
- The JS thread resolves the config. Inertia turns
animateandtransitioninto Reanimated animations once per change. The worklet only runs them. It does not read the config on each frame. - The worklets are memoized. A render with the same
animatemakes no new UI-thread closure. A regression test pins this. - Reanimated stays hidden. The public types use react-spring names. A
component that needs its own animated style imports the render-layer
primitives from
@rootnative/inertia/reanimated, so Inertia stays its only animation import.
Inertia is a declarative layer. Some patterns work better one layer down:
- UI that follows a finger frame by frame (sliders, swipe to dismiss,
pinch to zoom). Use
@rootnative/inertia-gestures, orreact-native-gesture-handlerwith raw Reanimated. - Data visualisation that changes on each frame. Keep the SVG attribute
interpolation in raw Reanimated, or use
@rootnative/inertia-svgto morph paths that have the same structure. - Custom physics. Use the hooks layer:
useMotionValue,useSpring, anduseTransform.
The hooks layer has the same shape as Reanimated, so the change of layer is small. Read the migration guide.
| Package | Install size | What it holds |
|---|---|---|
@rootnative/inertia |
The Motion.* primitives, the transitions, <Presence>, <MotionConfig>, <Stagger>, layout and shared elements, and the hooks. |
|
@rootnative/inertia-gestures |
useDrag, useSwipe, and usePan on react-native-gesture-handler. Optional. |
|
@rootnative/inertia-gradients |
MotionLinearGradient on expo-linear-gradient, with animatable colors, start, end, and locations. Optional. |
|
@rootnative/inertia-svg |
MotionPath and SVG shapes on react-native-svg, with animatable d (path morphing), fill, and stroke. Optional. |
The four packages ship in lockstep, with the same version number. Install the
same version of each @rootnative/inertia* package that you use.
Each release line supports one Expo SDK band. Install the line that matches the SDK of the app.
| Expo SDK | @rootnative/inertia and the adapters |
|---|---|
| 58 | 0.0.20 and later |
| 57 | 0.0.11 to 0.0.19 (npm install @rootnative/inertia@0.0.19) |
| 54 | 0.0.10 and earlier, with a lockfile that already resolved |
Each published package ships an llms.txt file with the
exact API of its version, for example
node_modules/@rootnative/inertia/llms.txt. The docs site serves
llms.txt, an overview, and
llms-full.txt, the full
API reference. The AI agents guide
has a CLAUDE.md snippet to copy.
| Page | Read it to |
|---|---|
| Introduction | Get an overview of the library and make a first animation |
| Installation | Install the peers, set up the Babel plugin, and learn the platform differences |
| Primitives | Learn each Motion.* component and the styles that it animates |
| Transitions | Choose a spring, a timing, or a decay, and see each animatable property |
| Variants | Name the states of a component and move between them |
| Sequences and repeat | Run steps one after the other, and repeat an animation |
| Presence | Animate a child out before it unmounts |
| Stagger | Delay each child by its position |
| Layout | Animate a change of layout, and move a shared element |
| Gestures | Animate the pressed, focused, and hovered states |
| Gestures adapter | Drag, swipe, and pan with Gesture Handler |
| Gradients | Animate a linear gradient |
| SVG | Morph an SVG path, and animate its fill and stroke |
| MotionConfig | Follow the reduce-motion setting, and name your transitions |
| Static export & SSR | Keep a pre-rendered page visible when the bundle does not run |
| Testing | Test animations in Jest with the preset |
| Hooks | Use the value layer: useMotionValue, useSpring, useTransform, and more |
| Migrating from raw Reanimated | Move Reanimated code to Inertia |
| Layer | Technology |
|---|---|
| Runtime | React 19.3, React Native 0.88, Expo SDK 58 |
| Animation | react-native-reanimated 4.7 and react-native-worklets 0.13, as peers |
| Language | TypeScript 5, strict mode |
| Build | tsup for the packages, Turborepo for the tasks |
| Package manager | pnpm 10, with the workspace protocol |
| Tests | Jest 29 and @testing-library/react-native |
| Docs | Docusaurus 3 |
| Delivery | GitHub Actions, GitHub Pages, and npm with trusted publishing |
You need Node.js 24 (the version in .nvmrc) and pnpm 10.
git clone https://github.com/rootnative/inertia.git
cd inertia
pnpm install| Command | What it does |
|---|---|
pnpm run build |
Builds each package with Turborepo |
pnpm run dev |
Builds each package in watch mode |
pnpm run typecheck |
Runs tsc --noEmit in each package |
pnpm run test |
Runs Jest in each package |
pnpm run lint |
Runs ESLint |
pnpm run format |
Formats the repository with Prettier |
pnpm run example |
Starts the Expo dev server of the example app |
pnpm run docs:dev |
Starts the docs site |
pnpm run build:llms |
Writes the llms.txt and llms-full.txt files again |
pnpm run check:versions |
Checks that each version reference agrees with the core package |
pnpm run clean |
Removes the build output and node_modules |
To start the example app on one platform, run pnpm run example:ios or
pnpm run example:android. Each primitive, sequence, variant, gesture state,
presence transition, and hook has its own screen in
example/screens/.
import { renderWithMotion } from '@rootnative/inertia/testing'
const { getByTestId } = renderWithMotion(
<Motion.View
testID="card"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
/>,
)
expect(getByTestId('card').props.style).toMatchObject({ opacity: 1 })The tests of each package are in its src/__tests__/ folder. The Reanimated
mock ends each animation at once, so a test asserts the final state, not a
frame between. The example app is where each frame is checked. Read
Testing for the full contract.
Repository layout
.
βββ docs/ # Docusaurus documentation site, with the live demo
βββ example/ # Expo app, one screen for each primitive and hook
βββ packages/
β βββ core/ # @rootnative/inertia: primitives, transitions, hooks, Presence, MotionConfig, layout
β βββ gestures/ # @rootnative/inertia-gestures: useDrag, useSwipe, usePan
β βββ gradients/ # @rootnative/inertia-gradients: MotionLinearGradient
β βββ svg/ # @rootnative/inertia-svg: MotionPath and SVG shapes
βββ scripts/ # Generators (llms.txt, brand) and the version check
βββ turbo.json
βββ pnpm-workspace.yaml
The code has no semicolons, single quotes, and trailing commas. ESLint uses a
flat config. Do not put a style object inline in JSX: move it to a const, a
useMemo, or StyleSheet.create. TypeScript is strict everywhere.
Three rules stop silent defects in code that runs on the UI thread:
- Make each callback that runs on the UI thread a worklet.
- Do not use JS-thread state inside a worklet. Go through a shared value or a
runOnJSboundary. - Keep
useAnimatedStyledeterministic. Read shared values in it, and do not change them.
After you edit a file, run Prettier on that file only
(npx prettier --write path/to/file.ts).
- Reanimated by Software Mansion runs every animation.
- The API takes ideas from Framer Motion and react-spring.
MIT Β© 2026 Root Native


