Skip to content

onlynative/inertia

Repository files navigation

Inertia

Node >=20 pnpm 10 Expo SDK 54 Reanimated 4 Turborepo License: MIT

Declarative animation primitives for React Native, built as a thin, ergonomic wrapper around react-native-reanimated. Inertia takes design cues from Framer Motion and React Spring and ships the props-driven surface — <Motion.View animate={{ ... }} transition={{ ... }} /> — without making you hand-roll worklets, shared values, or useAnimatedStyle.

Documentation  |  GitHub

Status: 0.0.1-alpha. The published surface is intentionally small while v0.1 acceptance criteria lock in. Pre-1.0 minor versions may break.

Features

  • Props-driven API — initial, animate, exit, transition, variants, gesture, controller, onAnimationEnd
  • Per-primitive style inference — Motion.View accepts ViewStyle, Motion.Text accepts TextStyle, Motion.Image accepts ImageStyle
  • React-spring vocabulary on the public surface (tension, friction, mass) — Reanimated's raw stiffness/damping never leak through
  • First-class sequences and keyframes with per-step transition overrides
  • State-machine animations via variants + string animate="open" (no hook required for the common case)
  • One gesture prop on every primitive (pressed, focused, focusVisible, hovered) — zero overhead when omitted
  • <Presence> for mount/unmount with auto pointerEvents: 'none' on exiting children
  • <MotionConfig reducedMotion="user"> respects OS reduce-motion settings end-to-end
  • JS-thread resolver, memoized worklets — re-renders with unchanged values produce zero new UI-thread closures
  • Subpath exports for tree-shaking (@onlynative/inertia/view, /text, /image, /pressable, /scroll-view)
  • TypeScript-first with strict mode and end-to-end inference

Install

yarn add @onlynative/inertia react-native-reanimated

Then follow the Reanimated install guide to enable its Babel plugin.

Peer dependencies — make sure these are installed in your project:

npx expo install react react-native react-native-reanimated

Optional — only needed for drag/swipe/pan gestures:

yarn add @onlynative/inertia-gestures react-native-gesture-handler

Optional — animated gradients (MotionLinearGradient):

yarn add @onlynative/inertia-gradients expo-linear-gradient

Optional — animated SVG path morphing:

yarn add @onlynative/inertia-svg react-native-svg

Quick Start

import { Motion } from '@onlynative/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 },
      }}
    />
  )
}

Tree-shaking when only one primitive is used:

import { MotionView } from '@onlynative/inertia/view'

See the docs for sequences, variants, gestures, <Presence>, and reduce-motion examples.

Transition shapes

type Public config keys Maps to
'spring' (default) tension, friction, mass, velocity, restSpeedThreshold, restDisplacementThreshold withSpring (converted from spring physics)
'timing' duration, easing, delay withTiming
'decay' velocity, deceleration, clamp withDecay
'no-animation' direct assignment, no interpolation

Plus, on any transition: delay, repeat (number | 'infinite' | { count, alternate }). Per-property transitions take precedence over the top-level transition.

Animatable properties — numeric: opacity, translateX, translateY, scale, scaleX, scaleY, rotate, rotateX, rotateY, width, height, borderRadius. Color: backgroundColor, borderColor, color, tintColor (Image only). Color targets are forwarded straight through withSpring / withTiming; Reanimated's value setter handles RGBA interpolation natively. Gradient interpolation lives in @onlynative/inertia-gradients; SVG path morphing lives in @onlynative/inertia-svg. Shared-element transitions across screens are deferred to v1.x.

When not to use Inertia

Inertia is a declarative wrapper. Some patterns work better one layer down:

  • Continuous gesture-driven UI (sliders, swipe-to-dismiss, pinch-zoom) — use @onlynative/inertia-gestures or drop down to react-native-gesture-handler + raw Reanimated.
  • Frame-by-frame data viz — keep SVG attribute interpolation in raw Reanimated, or use @onlynative/inertia-svg for declarative path morphing of structurally-compatible paths.
  • Custom physics — drop down to the useMotionValue / useSpring / useTransform hooks layer.
  • Layout / shared-element transitions — deferred to v1.x; use Reanimated's Layout API for now.

The hooks layer mirrors Reanimated's shape, so dropping down doesn't feel like switching tools. See the migration guide.

Packages

Package Description
@onlynative/inertia Animation primitives (Motion.*), transition resolvers, <Presence>, <MotionConfig>, useVariants.
@onlynative/inertia-gestures Optional react-native-gesture-handler adapter — useDrag, useSwipe, usePan.
@onlynative/inertia-gradients Optional expo-linear-gradient adapter — MotionLinearGradient with animatable colors / start / end / locations.
@onlynative/inertia-svg Optional react-native-svg adapter — MotionPath with animatable d (path morphing), fill, stroke.
example Expo Router app with one screen per primitive — manual validation harness.
docs Docusaurus documentation site. Hosts /llms.txt and /llms-full.txt.

Repository Layout

.
├── docs/                  # Docusaurus documentation site
├── example/               # Expo Router validation app (one screen per primitive)
├── packages/
│   ├── core/              # @onlynative/inertia
│   │   └── src/
│   │       ├── motion/        # Motion.View / Text / Image / Pressable / ScrollView + factory
│   │       ├── transitions/   # spring / timing / decay / no-animation resolvers
│   │       ├── values/        # useVariants (escape-hatch hooks layer)
│   │       ├── presence/      # <Presence>
│   │       ├── config/        # <MotionConfig>, reduce-motion gating
│   │       └── types.ts       # Public type surface
│   ├── gestures/          # @onlynative/inertia-gestures
│   │   └── src/           # useDrag / useSwipe / usePan (gesture-handler adapter)
│   ├── gradients/         # @onlynative/inertia-gradients
│   │   └── src/           # MotionLinearGradient (expo-linear-gradient adapter)
│   └── svg/               # @onlynative/inertia-svg
│       └── src/           # MotionPath + path utils (react-native-svg adapter)
├── scripts/               # build-llms.mjs (per-package + aggregated llms docs)
├── turbo.json
└── pnpm-workspace.yaml

Development

Requirements

  • Node.js >=20
  • pnpm 10.x
  • React 19.1+, React Native 0.81+, Expo SDK 54+
  • react-native-reanimated >=4.0.0 (peer)

Setup

pnpm install

Commands

Command Description
pnpm run build Build all packages with Turborepo
pnpm run dev Watch mode for all packages
pnpm run typecheck Type-check (tsc --noEmit) across packages
pnpm run test Jest across packages
pnpm run lint ESLint
pnpm run format Prettier (whole repo)
pnpm run example Start the Expo example app
pnpm run docs:dev Docusaurus dev server
pnpm run docs:build Build the docs site
pnpm run build:llms Regenerate llms.txt / llms-full.txt
pnpm run clean Clean build outputs and node_modules

After editing source, run Prettier on just the files you changed: npx prettier --write path/to/file.ts.

Running the Example App

pnpm run example            # Start Expo dev server

# Or target a specific platform
pnpm --filter @onlynative/inertia-example ios
pnpm --filter @onlynative/inertia-example android

Each Motion.* primitive, sequence behaviour, variant flow, gesture sub-state, presence transition, and the decay + reduce-motion paths have a dedicated screen under example/screens/. Frame-level animation correctness is validated here — Jest with the Reanimated mock can only assert final state.

Testing

import { renderWithMotion } from '@onlynative/inertia/testing'

const { getByTestId } = renderWithMotion(
  <Motion.View testID="card" initial={{ opacity: 0 }} animate={{ opacity: 1 }} />,
)
expect(getByTestId('card').props.style).toMatchObject({ opacity: 1 })

Tests live in src/__tests__/ per package. The Reanimated mock resolves animations synchronously — assert final state, not intermediate frames. See Testing for the full contract.

Tech Stack

Layer Technology
Runtime React 19.1, React Native 0.81+, Expo SDK 54+
Animation react-native-reanimated >=4.0.0 (peer)
Language TypeScript 5 (strict mode)
Build tsup (package bundling), Turborepo (task orchestration)
Package Manager pnpm 10 (workspace protocol)
Testing Jest 29, @testing-library/react-native
Documentation Docusaurus 3

Contributing

Conventions inherited from @onlynative/ui: no semicolons, single quotes, trailing commas; ESLint flat config; no inline style={{ ... }} objects in JSX (extract to a const / useMemo / StyleSheet.create); strict TypeScript everywhere.

When working on UI-thread code, three rules catch the silent breakages:

  1. Every callback that runs on the UI thread must be a worklet.
  2. Don't close over JS-thread state inside a worklet — go through a shared value or a runOnJS boundary.
  3. useAnimatedStyle must be deterministic — read shared values, don't mutate them.

After editing source, run Prettier on just the files you changed (npx prettier --write path/to/file.ts).

License

This project is licensed under the MIT License.

About

Wrapper around react native reanimated library to make the animation system easier

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors