Skip to main content

Hooks

The escape-hatch surface — drop here when you need imperative control beyond what the props expose.

The value-layer hooks (useMotionValue, useSpring, useBooleanSpring, useTransform, useShadow, useColorTransition, useInterpolatedStyle, useScroll) compose with useAnimatedStyle and every other Reanimated primitive — they return real shared values (or, in useShadow / useColorTransition / useInterpolatedStyle's case, an animated style fragment), not wrapped abstractions. Reach for them when an animation is gesture-driven, scroll-driven, or otherwise needs to live outside the declarative animate flow.

Unmount cleanup

useMotionValue, useSpring, useBooleanSpring, and useAnimation cancel any in-flight animation on unmount, so a still-settling or repeat: 'infinite' animation stops ticking its worklet once its owning component is gone — you don't need a manual cancelAnimation teardown effect for that case. To cancel mid-life (while the component stays mounted), import cancelAnimation from the /reanimated interop subpath and call it yourself. The Motion.* primitives get the same unmount cleanup for their internal shared values.

useMotionValue(initial)

Create an animatable value owned by JS but readable from worklets. A thin pass-through over Reanimated's useSharedValue — the returned SharedValue<T> works anywhere a shared value is accepted (useAnimatedStyle, useDerivedValue, the other value hooks below).

import { useMotionValue, Motion } from '@rootnative/inertia'
import { useAnimatedStyle } from 'react-native-reanimated'

function Draggable() {
const x = useMotionValue(0)

const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }],
}))

return (
<Motion.View
style={style}
onTouchMove={(e) => (x.value = e.nativeEvent.pageX)}
/>
)
}

We intentionally don't wrap the shared value in a MotionValue class — adding a { get, set, value } shell would force consumers to unwrap it at every Reanimated boundary and would break worklet capture. The bare shared value is the public surface.

SignatureReturns
useMotionValue<T extends number | string>(initial: T)SharedValue<T>

useSpring(target, config?)

Animate a shared value toward target with spring physics, using Inertia's react-spring vocabulary (tension / friction / mass).

target can be a plain number — the spring re-runs whenever the prop changes — or another SharedValue<number>, in which case the spring is driven by a UI-thread reaction. Both paths bottom out at the same withSpring call; the split is just which thread observes the source.

import { useMotionValue, useSpring, Motion } from '@rootnative/inertia'
import { useAnimatedStyle } from 'react-native-reanimated'

function Followable({ targetX }: { targetX: number }) {
// Plain-number path: re-springs whenever `targetX` changes.
const x = useSpring(targetX, { tension: 200, friction: 18 })

const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }],
}))
return <Motion.View style={style} />
}

function Chained() {
// SharedValue path: drag drives `dragX`, spring smooths it into `x`.
const dragX = useMotionValue(0)
const x = useSpring(dragX)

const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }],
}))
return <Motion.View style={style} />
}
SignatureReturns
useSpring(target: number | SharedValue<number>, config?: SpringTransition | TransitionName)SharedValue<number>

Config accepts every field of SpringTransition: tension, friction, mass, velocity. Reanimated's raw stiffness / damping names are never accepted — that conversion is private to the library.

config also accepts a named transition. Because the hook is spring-only, the name must be registered as a spring — a timing / decay / no-animation name warns in dev and falls back to the default spring (use useAnimation to honor the registered type). The same applies to useBooleanSpring.

Reduced motion is honoured. Under <MotionConfig reducedMotion> the hook assigns the target to the output directly, with no spring, on both the plain-number path and the shared-value path. The same applies to useBooleanSpring.

useBooleanSpring(active, config?)

Sugar over useSpring for the recurring "spring 0↔1 progress from a boolean" shape — checkbox checks, accordion expansions, drawer open/closed states, focus rings, and every other binary UI flip that wants spring physics rather than a hard cut.

import {
useBooleanSpring,
useColorTransition,
useShadow,
} from '@rootnative/inertia'

function Card({ raised }: { raised: boolean }) {
// One driver, multiple downstream styles. The same progress feeds the
// shadow tween and the surface-color shift in lockstep.
const progress = useBooleanSpring(raised, { tension: 180, friction: 18 })

const shadowStyle = useShadow({
from: REST_SHADOW,
to: RAISED_SHADOW,
progress,
})
const fillStyle = useColorTransition(progress, ['#ffffff', '#f8fafc'])

return <Motion.View style={[styles.card, fillStyle, shadowStyle]} />
}

The returned shared value sits at 0 when active is false and animates toward 1 when it flips to true (and back on the reverse flip). The spring re-runs whenever active changes.

SignatureReturns
useBooleanSpring(active: boolean, config?: SpringTransition | TransitionName)SharedValue<number>

Config follows the same react-spring vocabulary as useSpring (tension / friction / mass); omit it for the library defaults. Note that like useSpring, this hook does not consult <MotionConfig reducedMotion> — it always interpolates. If the value must respect reduced motion, drive it through useAnimation (which gates on the config) or gate the target yourself with useShouldReduceMotion().

useBooleanSpring vs useSpring(active ? 1 : 0). Identical mechanics — useBooleanSpring is the named version of the pattern so the call site reads as the boolean it represents, not as a ternary. Reach for it whenever the source is a boolean prop and the target is a 0↔1 progress value to feed into useShadow / useColorTransition / useTransform / a hand-rolled useAnimatedStyle.

useBooleanSpring vs useAnimation(active ? 1 : 0, { type: 'spring', ... }). Same animation either way; useBooleanSpring is the shorter spelling for the spring-only case. Drop to useAnimation if you need a timing curve, a decay, or a repeat.

useTransform(...)

Derive a value from one or more shared values. Two overloads:

Interpolation

Map a numeric shared value onto a range of numbers or colors. Output type drives whether the underlying call is interpolate (numerics) or interpolateColor (color strings).

import { useMotionValue, useTransform } from '@rootnative/inertia'

const scroll = useMotionValue(0)
const headerOpacity = useTransform(scroll, [0, 100], [1, 0])
const headerTint = useTransform(scroll, [0, 100], ['#ffffff', '#000000'])
SignatureReturns
useTransform(value: SharedValue<number>, inputRange: number[], outputRange: number[], options?: UseTransformOptions)SharedValue<number>
useTransform(value: SharedValue<number>, inputRange: number[], outputRange: string[], options?: UseTransformOptions)SharedValue<string>

UseTransformOptions:

FieldTypeDefaultNotes
extrapolateLeft'clamp' | 'identity' | 'extend''clamp'Behavior below the first input value.
extrapolateRight'clamp' | 'identity' | 'extend''clamp'Behavior above the last input value.

The input range must be monotonically increasing.

Transformer worklet

Derive any value from any number of shared values via a worklet.

const x = useMotionValue(0)
const y = useMotionValue(0)
const distance = useTransform(() => {
'worklet'
return Math.sqrt(x.value ** 2 + y.value ** 2)
})
SignatureReturns
useTransform<T>(transformer: () => T)SharedValue<T>

The transformer must be a worklet — add the 'worklet' directive as its first statement so your Babel plugin captures the shared values it reads as the worklet's closure. A plain function warns in dev and cannot track dependencies: the fallback wrapper closes over the opaque function reference, not the shared values read inside it, so the derived value only refreshes on React re-renders (and native builds reject it outright). The transformer must be pure: no captured JS-thread refs, no calls to non-worklet APIs.

useShadow({ from, to, progress })

Interpolate between two shadow configs as progress moves 0→1. Returns an animated style fragment you spread onto any Reanimated-aware view (Motion.* or a hand-rolled Animated.View) — no other props required on the host.

import { Motion, useShadow, useSpring } from '@rootnative/inertia'

function ElevatedCard({ raised }: { raised: boolean }) {
// Whatever drives `progress` is your concern — a spring, a scroll-derived
// useTransform, a gesture progress value. The hook is a pure interpolator;
// it does not animate on its own.
const progress = useSpring(raised ? 1 : 0, { tension: 180, friction: 18 })

const shadowStyle = useShadow({
from: {
shadowOpacity: 0.08,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
elevation: 1,
},
to: {
shadowOpacity: 0.24,
shadowRadius: 16,
shadowOffset: { width: 0, height: 10 },
elevation: 10,
},
progress,
})

return <Motion.View style={[styles.card, shadowStyle]} />
}

ShadowConfig accepts the same flat shadow keys as Motion.View's animate surface, plus the nested shadowOffset source:

FieldTypeNotes
shadowOpacitynumberStandard iOS shadow opacity.
shadowRadiusnumberBlur radius in points.
shadowOffset{ width?: number; height?: number }Single-value form — width/height each tween independently.
elevationnumberAndroid elevation. iOS-only consumers can leave it off.
shadowColorstringColor string. Interpolated via interpolateColor.
boxShadowstring | BoxShadowLayer[]CSS box-shadow — the shadow surface on web and RN 0.76+ new-arch native.

Only keys present on at least one side appear in the output style — there's no overhead for keys you don't use. A key present on one side and absent from the other tweens from / to the absent side's natural zero (0, 'transparent', or { width: 0, height: 0 }).

Web shadows: boxShadow

The classic shadow* / elevation keys never reach the web renderer — react-native-web's elevation surface is the CSS box-shadow string. boxShadow closes that gap: pass the CSS string form your design system already stores elevation tokens in (px lengths only), or structured BoxShadowLayer objects ({ offsetX, offsetY, blurRadius?, spreadDistance?, color?, inset? }, mirroring RN's BoxShadowValue):

const shadowStyle = useShadow({
from: {
boxShadow: '0px 1px 2px rgba(0,0,0,0.30), 0px 1px 3px 1px rgba(0,0,0,0.15)',
},
to: {
boxShadow: '0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px rgba(0,0,0,0.30)',
},
progress,
})

Multi-layer shadows interpolate per layer; when one side has fewer layers (including 'none'), it's padded with invisible layers, CSS-transition style. Like cubicBezier, a malformed string throws at render rather than warning — shadow tokens are built at theme setup, and a typo should fail loudly there. Two paired layers with mismatched inset are not interpolable and also throw. The interpolated value is emitted as a boxShadow style string, which react-native-web passes through as CSS and React Native 0.76+ (new architecture) renders natively; on old-architecture native it's ignored, so keep the shadow* / elevation keys alongside it for those targets.

SignatureReturns
useShadow({ from: ShadowConfig; to: ShadowConfig; progress: SharedValue<number> })Animated style (spread onto style).

useShadow vs the declarative animate path. If a state change in your component already swaps between two shadow configs (animate={raised ? RAISED : REST}), the animate prop is simpler — no hook needed. Reach for useShadow when the source of progress isn't a render-driven state swap: scroll position, drag progress, a useSpring driven by a shared value, or anywhere else the value-layer is the right abstraction. The example screen in the gallery drives it from a useSpring; wiring it to useScroll + useTransform for a scroll-driven elevation cascade is a one-line change.

useColorTransition(progress, [from, to], options?)

Interpolate a single color channel between from and to as progress moves 0→1. Returns an animated style fragment with one color key — spread it onto any Reanimated-aware view alongside other style fragments (a useShadow result, a hand-rolled useAnimatedStyle, plain StyleSheet entries).

import {
Motion,
useBooleanSpring,
useColorTransition,
} from '@rootnative/inertia'

function Chip({ active }: { active: boolean }) {
// Drive the tween from a boolean spring. The same progress feeds two
// independent color channels — fill and ring shift in lockstep.
const progress = useBooleanSpring(active, { tension: 200, friction: 18 })

const fillStyle = useColorTransition(progress, ['#e5e7eb', '#4f46e5'])
const ringStyle = useColorTransition(progress, ['#d1d5db', '#312e81'], {
key: 'borderColor',
})

return <Motion.View style={[styles.chip, fillStyle, ringStyle]} />
}

The hook is a pure interpolator; it does not animate on its own. Drive progress upstream with a useSpring, useBooleanSpring, scroll-derived useTransform, gesture progress, or anything else producing a 0↔1 shared value. Values outside [0, 1] clamp.

options.key chooses which RN style slot the interpolated color is emitted under:

KeyDefaultNotes
backgroundColorState-layer haloes, card fills, chip surfaces.
colorAnimate text color (combine with Motion.Text or a plain text).
borderColor / borderTopColor / borderRightColor / borderBottomColor / borderLeftColorFocus rings, outline transitions.
tintColorMotion.Image tint.
shadowColorShadow color shifts. (For full shadow tweens, prefer useShadow.)
SignatureReturns
useColorTransition(progress: SharedValue<number>, range: readonly [string, string], options?: UseColorTransitionOptions)Animated style (spread onto style).

useColorTransition vs useTransform with a color output range. useTransform(progress, [0, 1], [from, to]) returns a raw SharedValue<string> — reach for it when you need to compose the color into a hand-rolled useAnimatedStyle (mixing with a Math.max(...) halo opacity, feeding into a gradient, etc.). useColorTransition is the one-call convenience for the dominant case: tween a single color slot and spread the result onto style.

useColorTransition vs the declarative animate path. If a state change already swaps between two colors (animate={active ? { backgroundColor: A } : { backgroundColor: B }}), the animate prop is simpler. Reach for useColorTransition when the source of progress isn't a render-driven state swap, or when several color slots need to shift in lockstep from one shared driver (the chip example above — one spring, three color channels).

useColorCascade(rest, layers, options?)

Composite a priority-ordered stack of color layers over a base rest color. Each layer owns its own progress shared value and blends toward its color as that progress rises; later layers win over earlier ones at equal progress. This is the values-layer form of the same fixed-priority cascade the gesture prop uses (Decision 5), for state colors that don't come from gestures — error, selected, activated, dragged.

import { Motion, useBooleanSpring, useColorCascade } from '@rootnative/inertia'

function Field({ hovered, errored, focused }: FieldStates) {
const hoverP = useBooleanSpring(hovered)
const errorP = useBooleanSpring(errored)
const focusP = useBooleanSpring(focused)

// Priority order, lowest first — focus wins over error wins over hover,
// each animating in on its own spring.
const borderStyle = useColorCascade(
colors.border,
[
{ progress: hoverP, color: colors.borderHover },
{ progress: errorP, color: colors.borderError },
{ progress: focusP, color: colors.borderFocus },
],
{ key: 'borderColor' },
)

return <Motion.View style={[styles.field, borderStyle]} />
}

The chain is exactly equivalent to the hand-written nested-interpolateColor shape focus(error(hover(rest))), collapsed into one hook and one worklet. layers is priority order, lowest first (matching the gesture cascade). Each layer's progress is a 0↔1 shared value you drive upstream (a useSpring, useBooleanSpring, gesture progress, …) — the hook is a pure interpolator and does not animate on its own.

options.key reuses the same ColorStyleKey set as useColorTransition, defaulting to backgroundColor.

SignatureReturns
useColorCascade(rest: string, layers: readonly ColorCascadeLayer[], options?: UseColorCascadeOptions)Animated style (spread onto style).

where ColorCascadeLayer is { progress: SharedValue<number>; color: string }.

Rewiring a layer. The chain is resolved on the JS thread and kept identity-stable, so passing a fresh-but-equal layers array every render costs nothing. Changing a colour, the key, the base rest, the layer count, or which shared value drives a layer all rewire the worklet — so a conditionally-sourced progress ({ progress: isError ? errorSpring : hoverSpring, color }) behaves as written.

useColorCascade vs useColorTransition. useColorTransition(progress, [rest, active]) is the single-layer fast path — one progress ⇄ one color slot. Reach for useColorCascade when independent states each contribute a color and their precedence matters (a field border that is simultaneously hoverable, error-able, and focusable). It is not a replacement for the single-layer hook.

Numeric cascades. useColorCascade is color-only by design. If a numeric key needs the same precedence (e.g. an outlined border widening 1→2dp under error/focus), compose it separately — drive each layer through useInterpolatedStyle and combine with a useTransform Math.max(...), or drop to a hand-rolled useAnimatedStyle.

useInterpolatedStyle(progress, map, options?)

Map one progress shared value onto N style props at once, returning an animated style fragment. The style-fragment counterpart to useTransform's output-range form: where useTransform gives you one interpolated value to compose by hand, useInterpolatedStyle interpolates a whole map of keys and hands back a ready-to-spread fragment. Reach for it when a single driver (a collapse progress, a float progress, a scroll offset) fans out to several style props — the shape that otherwise becomes a hand-rolled useAnimatedStyle over the Reanimated interop subpath.

import {
Motion,
useBooleanSpring,
useInterpolatedStyle,
} from '@rootnative/inertia'

function FloatingLabel({ focused }: { focused: boolean }) {
const progress = useBooleanSpring(focused, { tension: 200, friction: 18 })

// One driver → a translate + scale float, lifted into a transform array.
const labelStyle = useInterpolatedStyle(progress, {
translateY: [12, 0],
scale: [1, 0.75],
})

return <Motion.Text style={[styles.label, labelStyle]}>Email</Motion.Text>
}

The hook is a pure interpolator; it does not animate on its own. Drive progress upstream with a useSpring, useBooleanSpring, scroll-derived useTransform, gesture progress, or anything else producing a shared value.

Key routing

  • Numeric style keys (opacity, height, width, borderRadius, fontSize, top, …) take number stops and route through interpolate, emitted as top-level style keys.
  • Transform keys (translateX, translateY, scale, scaleX, scaleY, rotate, rotateX, rotateY) take number stops and are lifted into a single transform array, in the order they appear in the map — the same key-order convention the animate prop uses. rotate* keys take numeric degrees and emit '<n>deg' strings.
  • Color keys (backgroundColor, color, borderColor and its per-edge variants, tintColor, shadowColor) take color-string stops and route through interpolateColor — a multi-stop useColorTransition without leaving this hook. Stop types are checked at compile time: number stops on a color key (or color stops on a numeric/transform key) are a type error.

Options

OptionDefaultNotes
inputRange[0, 1] for 2-stop outputs; evenly-spaced across [0, 1] otherwiseApplies to every key. Provide it for multi-stop curves; its length must match each output length (dev-warns otherwise).
extrapolate'clamp'Edge behavior outside the input range for numeric keys. Color interpolation always clamps.
// Multi-stop, custom input range — fade in then out across a scroll span.
const fade = useInterpolatedStyle(
scrollProgress,
{ opacity: [0, 1, 1, 0] },
{ inputRange: [0, 0.2, 0.8, 1] },
)
SignatureReturns
useInterpolatedStyle(progress: SharedValue<number>, map: InterpolatedStyleMap, options?: UseInterpolatedStyleOptions)InterpolatedStyle<K> — an animated style fragment typed from the map's own keys.

The map is memoized on an order-preserving signature, so a fresh-but-equal map literal each render produces zero new UI-thread closures (the factory rule).

The return type follows the map, so no cast is needed in a style array. A map of view keys yields a fragment that satisfies StyleProp<ViewStyle>; text-metric keys (fontSize, lineHeight, letterSpacing) yield one that satisfies StyleProp<TextStyle>, and tintColor one that satisfies StyleProp<ImageStyle>. Transform keys collapse into the single transform array the hook emits.

const card = useInterpolatedStyle(scrollX, {
scale: [0.84, 1, 0.84],
opacity: [0.45, 1, 0.45],
})

// Narrows to ViewStyle — no `as ViewStyle` at the call site.
<Motion.View style={[styles.slot, card]} />

This matters because Reanimated's own useAnimatedStyle resolves to DefaultStyle (ViewStyle | ImageStyle | TextStyle). A style array checks every member of that union, and TextStyle fails against ViewStyle on cursor — so a union return forced consumers to cast. The narrowing is exact rather than merely permissive: a text-only fragment is still rejected from a ViewStyle slot.

useInterpolatedStyle vs useTransform. useTransform(progress, [0, 1], [a, b]) returns one raw SharedValue for you to compose. useInterpolatedStyle is the convenience for the dominant case — several style/transform keys driven from one progress value, returned as a spreadable fragment. For function-valued entries or multi-source composition (Math.max(a, b)), drop to useTransform's worklet form or a hand-rolled useAnimatedStyle — this hook stays fully declarative and hashable by design.

useAnimation(target, transition?)

The general-purpose value-layer hook: drive a SharedValue<number> toward target with any TransitionConfig. Reach for it when you need raw useSharedValue + useEffect + withTiming (or withSpring, or withRepeat) outside the declarative animate flow — boolean state progress on a widget with multiple animated children, indeterminate progress on a list of useAnimatedStyle consumers, anywhere the value layer is the right abstraction.

import { useAnimation } from '@rootnative/inertia'

// Toggle progress driven by a prop. Spring physics, react-spring vocab.
const progress = useAnimation(isChecked ? 1 : 0, {
type: 'spring',
tension: 380,
friction: 33,
})

// Float a label when the field has a value. Timing curve.
const floated = useAnimation(hasValue ? 1 : 0, {
type: 'timing',
duration: 150,
})

// Indeterminate progress slider — loops forever, snaps back each cycle.
const slide = useAnimation(1, {
type: 'timing',
duration: 1800,
repeat: { count: 'infinite', alternate: false },
})
SignatureReturns
useAnimation(target: number, transition?: TransitionConfig | TransitionName)SharedValue<number>

The hook re-runs the animation whenever target changes or the transition's structural signature changes. A fresh literal each render ({ type: 'timing', duration: 200 } rebuilt on every call) doesn't re-fire — only structural changes do. Reduced motion (<MotionConfig reducedMotion>) collapses the transition to 'no-animation' and snaps the value.

transition also accepts a named transition; unlike the spring-only hooks, the registered config's type is honored whatever it is.

useAnimation vs useSpring. They overlap on the spring case — useAnimation(target, { type: 'spring', ... }) and useSpring(target, { ... }) produce the same animation. Prefer useSpring when you only want spring physics; it also accepts a SharedValue<number> as the target for UI-thread-reactive smoothing (a gesture-driven smoothing source). useAnimation is the general-purpose hook — accepts any TransitionConfig including timing, decay, no-animation, and repeat — but is JS-thread-driven only.

useAnimation vs Motion.View animate={{...}}. Use the animate prop when one Motion primitive owns the animated value end-to-end. Use useAnimation when the same value drives several useAnimatedStyle blocks across siblings — there's no Motion primitive to attach animate to in that case.

useAnimator()

The imperative counterpart to useAnimation. useAnimation drives a value from an effect (it re-runs when target changes); useAnimator returns a setter you call from an event handler to push a value toward a target on demand. Reach for it when the write is driven by an event — a hover-in, a focus, a press — rather than by rendered state.

import { useAnimator, useMotionValue, Motion } from '@rootnative/inertia'

function HoverCard() {
const hovered = useMotionValue(0)
const animate = useAnimator()

return (
<Motion.Pressable
onHoverIn={() => animate(hovered, 1, 'state-hover')}
onHoverOut={() => animate(hovered, 0, 'state-hover')}
/>
)
}

It exists because the hand-written escape hatch — value.value = resolveTransition(config, to) from an event handler — has two footguns that useAnimator closes:

  • Named transitions resolve. A named transition registered on the nearest <MotionConfig transitions> works here just as it does on the transition prop or in useAnimation. A bare resolveTransition call can't reach the registry (names resolve through context), so imperative call sites otherwise rebuild configs the provider already owns.
  • Reduced motion is respected. The write routes through the same 'no-animation' downgrade useAnimation applies under <MotionConfig reducedMotion>. Hand-rolled resolveTransition writes silently bypass that accessibility setting — a correctness bug this hook fixes.

The returned callback is identity-stable for the lifetime of the component — it reads the registry and the reduced-motion flag out of refs at call time, so neither a provider republishing its transitions map nor a reduced-motion change hands you a new function. Drop it into memoized handlers or a useCallback dependency list without churning them. Reading at call time also means a write always resolves against the registry current when the event fires, not the one captured at render.

SignatureReturns
useAnimator()Animator
Animator = (value: SharedValue<number>, to: number, transition?: TransitionConfig | TransitionName) => voidvoid

transition defaults to the library spring when omitted, and accepts any TransitionConfig or a registered TransitionName — the same input the declarative transition prop takes.

useAnimator vs useAnimation. Same transition vocabulary, opposite trigger. useAnimation(target, transition) re-fires from an effect whenever target changes; useAnimator()(value, to, transition) fires exactly when you call it. Use useAnimation for state-derived values, useAnimator for event-driven writes.

This is not a new animation API — it starts animations in Inertia's existing transition vocabulary, so it doesn't conflict with the "no imperative-only APIs that bypass the declarative surface" scope rule. It's the hooks-layer equivalent of useMotionValue + resolveTransition, minus the footguns.

useScroll()

Track the scroll offset of a Motion.ScrollView or a Motion.FlatList as shared values. Scroll events fire on the UI thread, so the returned values are safe to read from any worklet without a JS-thread bounce.

import { useScroll, useTransform, Motion } from '@rootnative/inertia'
import { useAnimatedStyle } from 'react-native-reanimated'

function StickyHeader() {
const { scrollY, onScroll } = useScroll()
const headerOpacity = useTransform(scrollY, [0, 100], [1, 0])

// Shared values feed `useAnimatedStyle`, not `animate`. The `animate` prop
// takes plain values that the resolver bakes into `withSpring` /
// `withTiming` calls on the JS thread; shared values are the right fit
// for continuous, externally-driven inputs (scroll, gestures).
const headerStyle = useAnimatedStyle(() => ({ opacity: headerOpacity.value }))

return (
<>
<Motion.View style={headerStyle} />
<Motion.ScrollView onScroll={onScroll} scrollEventThrottle={16}>
{/* …content… */}
</Motion.ScrollView>
</>
)
}

Returns:

FieldTypeNotes
scrollXSharedValue<number>Horizontal scroll offset in points.
scrollYSharedValue<number>Vertical scroll offset in points.
onScroll(event: NativeSyntheticEvent<NativeScrollEvent>) => voidPass to the scrollable's onScroll prop. Worklet-backed; safe to forward to any Reanimated-animated scrollable.

Set scrollEventThrottle={16} on a Motion.ScrollView for steady 60 Hz updates; without it, Android can dispatch less frequently than iOS. Motion.FlatList needs no such prop — it defaults the throttle to 1. Reach for Motion.FlatList when the list is long enough that mounting every row costs more than the animation does.

useGesture(transition?)

The hook-form of the gesture prop. Reach for it when one Pressable's gesture state needs to drive multiple animated views — a focus ring rendered as a sibling, an MD3 state-layer halo that overlays the content, a content tint and a separate icon-color animation, etc. The prop-form only animates the receiver's own style; the hook gives you the underlying 0↔1 progress shared values to feed into any number of useAnimatedStyle blocks.

import { useGesture } from '@rootnative/inertia'
import Animated, {
interpolateColor,
useAnimatedStyle,
} from 'react-native-reanimated'

function StateLayerButton() {
const { pressed, focused, focusVisible, hovered, handlers } = useGesture({
pressed: { type: 'timing', duration: 100 },
hovered: { type: 'timing', duration: 150 },
focused: { type: 'timing', duration: 200 },
})

const ringStyle = useAnimatedStyle(() => ({ opacity: focusVisible.value }))
const haloStyle = useAnimatedStyle(() => ({
opacity: Math.max(
hovered.value * 0.08,
focused.value * 0.1,
pressed.value * 0.1,
),
}))

return (
<Pressable {...handlers}>
<Animated.View pointerEvents="none" style={ringStyle} />
<Animated.View pointerEvents="none" style={haloStyle} />
</Pressable>
)
}

Returns:

FieldTypeNotes
pressedSharedValue<number>0↔1 progress for the pressed layer.
focusedSharedValue<number>0↔1 progress for any focus modality.
focusVisibleSharedValue<number>0↔1 progress for keyboard-only focus (W3C :focus-visible semantics).
hoveredSharedValue<number>0↔1 progress for hover (web only — stays at 0 on native).
handlersUseGestureHandlers{ onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur }. Spread on the host Pressable.

Transitions follow the same shape as the gesture prop's accompanying transition:

  • useGesture({ type: 'timing', duration: 150 }) — same config for every layer.
  • useGesture({ pressed: {...}, hovered: {...}, focused: {...}, focusVisible: {...} }) — per-layer.
  • A named transition is accepted in both positions: useGesture('state-press') or useGesture({ pressed: 'state-press' }).
  • Layers without an explicit transition fall back to the library default spring.
  • <MotionConfig reducedMotion> is respected — when reduced motion is active, every transition collapses to 'no-animation' and progress snaps instead of interpolating.

The shared values and the handler bag are identity-stable across renders — safe to pass to memoized children. To compose with consumer handlers (e.g. your own onPressIn for analytics), wrap manually:

<Pressable
{...handlers}
onPressIn={(e) => {
track('press')
handlers.onPressIn()
}}
/>

When useGesture and the gesture prop describe the same scenario, prefer the prop — fewer moving parts. The hook is the escape hatch for compositions the prop can't express.

useGestureLayer(states, options?)

A higher-level helper over useGesture for the strongest-active-layer-wins composition model used by MD3 state-layer haloes and iOS-translucent overlays. You supply per-state target maps; the hook owns the four gesture progress shared values, the disabled override, the worklet, and the transition. It also hands those progress shared values back as states, so styles derived from the same gesture wiring (an elevation crossfade, an icon tint) don't need a second useGesture call. Lives at the @rootnative/inertia/gesture-layer subpath so apps that don't need it don't pay for it.

import { Pressable } from 'react-native'
import Animated from 'react-native-reanimated'
import { useGestureLayer } from '@rootnative/inertia/gesture-layer'

function SwitchHalo({ disabled }: { disabled?: boolean }) {
const { style, handlers } = useGestureLayer(
{
rest: { opacity: 0, backgroundColor: 'transparent' },
hovered: { opacity: 0.08, backgroundColor: '#000' },
focused: { opacity: 0.1, backgroundColor: '#000' },
pressed: { opacity: 0.12, backgroundColor: '#000' },
},
{ disabled, transition: { type: 'timing', duration: 150 } },
)

return (
<Pressable {...handlers}>
<Animated.View style={style} />
</Pressable>
)
}

Composition model

  • Numeric keys (opacity, scale, borderWidth, …) compose via clamped-max with rest as the floor: out = max(rest, …for each active layer: lerp(rest, layer, progress)). Multiple states active simultaneously raise the value to the strongest layer, not the sum — this is the MD3 halo pattern.
  • Color keys (any string value) compose via priority cascade with interpolateColor, lowest priority first: hovered → focused → focusVisible → pressed. Clamped-max doesn't apply to colors; this matches the cascade used by the declarative gesture prop.
  • Disabled sits at the top of the cascade for both numeric and color keys — when active, it lerps the composed value toward the disabled target. The disabled flag is JS-side, not gesture-driven.

States

KeyNotes
restBase values, applied when no other layer is active. Missing keys default to 0 (numeric) / 'transparent' (color).
hoveredWeb-only hover. No-op on native.
focusedAny focus modality (mouse, touch, keyboard).
focusVisibleKeyboard-only focus (W3C :focus-visible). On native, behaves identically to focused.
pressedActive touch / pointer-down.
disabledGated by options.disabled. Overrides every gesture layer when active; per-layer transitions don't apply (top-level transition or default spring).

Every state key is optional. Values inside each state are a flat map of style keys to either a number (numeric layer) or a string (color layer). The hook doesn't validate that string values are valid colors — passing borderStyle: 'solid' will crash inside the worklet. Keep string values to color strings.

Options

FieldTypeNotes
disabledbooleanActivates the disabled layer (or rest if disabled is undefined).
transitionTransitionConfig | TransitionName | GestureLayerTransitionsForwarded to the underlying useGesture. Either one config for every gesture layer, or per-layer ({ pressed, focused, focusVisible, hovered }). Reduced motion collapses every transition to 'no-animation'. Per-layer transitions don't apply to disabled — it uses the top-level config (or the default spring).

Returns

FieldTypeNotes
styleAnimated styleSpread onto an Animated.View or a Motion.*'s style.
handlersUseGestureHandlers{ onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur }. Spread on the host Pressable.
statesGestureLayerProgressThe per-state 0↔1 progress shared values the style is derived from: { hovered, focused, focusVisible, pressed, disabled }. Stable across renders; treat as read-only — the handlers own the writes.

states exists so a component that already wires its state layer through useGestureLayer can derive additional styles from the same gesture progress — without duplicating the gesture wiring through a parallel useGesture call. The canonical case is an elevation crossfade on hover:

import { useGestureLayer } from '@rootnative/inertia/gesture-layer'
import { useShadow } from '@rootnative/inertia'

const { style, handlers, states } = useGestureLayer(layerStates, { disabled })
// Feed the raw hover progress straight into another progress-driven hook:
const shadow = useShadow({
from: restShadow,
to: raisedShadow,
progress: states.hovered,
})
// <Animated.View style={[style, shadow]} />

The gesture entries (hovered / focused / focusVisible / pressed) are the same shared values useGesture returns; disabled is the hook's own 0↔1 progress for the options.disabled override. Any of them also plug into a custom useAnimatedStyle or useDerivedValue for derivations the composed style can't express.

useGestureLayer vs the declarative gesture prop. The prop composites states as a fixed priority cascade (hovered → focused → focusVisible → pressed) — when two layers are fully active, the value converges to the higher-priority layer's target, regardless of which is numerically stronger. useGestureLayer composites numerics via clamped-max — pressing while hovered shows whichever target is stronger per-key, whatever its priority. The MD3 / iOS-translucent halo wants clamped-max so a weaker press target can't visually "dim" an active hover layer. Reach for the prop for ordinary button feedback ({ scale: 0.96 } on press layering over an opacity hover); reach for this hook when targets are state-layer overlays whose strongest value should win.

useGestureLayer vs useGesture + custom worklet. useGesture returns the raw 0↔1 progress shared values. Use it for compositions this hook doesn't express — additive blends, per-key custom rules, multi-target outputs (e.g. a halo plus a separate ring that uses different math). useGestureLayer is the convenience layer for the one composition model it owns.

useVariants(variants, initial?)

Build a controller for a variants map. Pass it through controller={...} to drive transitions imperatively.

import { useVariants } from '@rootnative/inertia'

const variants = {
open: { opacity: 1, translateY: 0 },
closed: { opacity: 0, translateY: 100 },
} as const

const controller = useVariants(variants, 'closed')
controller.transitionTo('open')
console.log(controller.current) // 'open'

Returns { current, transitionTo, subscribe }:

FieldTypeNotes
currentkeyof V & stringActive variant key. Read-only — change it via transitionTo.
transitionTo(next: keyof V & string) => voidNo-op if next === current. Warns on unknown keys in dev.
subscribe(listener) => () => voidInternal. Motion primitives subscribe via the controller prop.

The controller is identity-stable — the hook returns the same object across renders. It also pins the variants map it received on the first render: if a later render passes a different map object, the controller keeps validating keys against the first one. Define the map once (module scope or useMemo) rather than inline in render.

Do not set controller and animate on the same primitive — the controller drives the animation and animate is ignored (warns in dev).

See Variants for the props-side usage.

useMotionConfig()

Read the active <MotionConfig> value:

import { useMotionConfig } from '@rootnative/inertia'

const { reducedMotion, transitions } = useMotionConfig()
// reducedMotion: 'user' | 'never' | 'always'
// transitions: the merged named-transition registry from ancestor providers

Returns the default ({ reducedMotion: 'user', transitions: {} }) when no provider is in the tree.

useShouldReduceMotion()

Resolve the active reduced-motion mode to a boolean. 'user' consults Reanimated's OS-backed hook; 'never' and 'always' shortcut to false / true.

import { useShouldReduceMotion } from '@rootnative/inertia'

function MyVideoIntro() {
const reduce = useShouldReduceMotion()
if (reduce) return <StaticPoster />
return <AnimatedIntro />
}

This is what every Motion primitive uses to decide whether to swap transitions for 'no-animation'. Subscribes to OS changes — components re-render when the user toggles the setting at runtime.

usePresence()

Read the per-child presence context inside a custom component you'd like to behave like a Motion primitive under <Presence>:

import { usePresence } from '@rootnative/inertia'

function CustomExitable() {
const presence = usePresence()
// presence is null when no <Presence> ancestor exists
// presence?.isPresent flips false when the parent removes us
// presence?.safeToRemove() — call once exit completes
}

See Presence for the higher-level prop-driven usage.

buildReleaseAnimation(transition, toValue, callback?)

Worklet-safe single-step animation builder. Mirrors a subset of resolveTransition for the UI-thread path where the transition config is picked at gesture-release time, not at render time. Supports spring / timing / decay / no-animation; sequences, top-level repeat, and easing-function auto-worklet-wrapping are not.

import { buildReleaseAnimation } from '@rootnative/inertia'
import { Gesture } from 'react-native-gesture-handler'

const pan = Gesture.Pan().onEnd((e) => {
'worklet'
// Spring to a snap target with the release velocity preserved.
position.value = buildReleaseAnimation(
{ type: 'spring', velocity: e.velocityX, friction: 26, tension: 170 },
snapTarget,
) as number
})

For decay transitions, the second argument is ignored — decay decelerates from the SV's current position via its own physics.

callback, when provided, fires once when the animation settles — the same (finished) => void shape Reanimated's with* factories accept. It runs on the UI thread; bridge to JS-thread code with runOnJS(...) inside it. For no-animation it fires synchronously with finished: true, since a direct assignment has no settle point of its own. This is what useSwipe's onSwipeEnd rides on.

SignatureReturns
buildReleaseAnimation(transition: TransitionConfig, toValue: number, callback?: AnimationCallback)Reanimated animation value (assign to a SharedValue<number> directly).

Most consumers reach for useDrag({ onRelease }) from @rootnative/inertia-gestures instead — it wraps this builder behind a per-axis return shape. Use buildReleaseAnimation directly when you're authoring a custom Gesture.*().onEnd(...) worklet outside the adapter hooks.

useTranslateStyle(x, y)

Animated style that translates by two shared values: transform: [{ translateX: x }, { translateY: y }]. This is the animatedStyle every drag-style hook returns (useTouchDrag, and useDrag / usePan / useSwipe in @rootnative/inertia-gestures). Use it directly when a custom gesture owns its own translation values.

import { Motion, useMotionValue, useTranslateStyle } from '@rootnative/inertia'

function Puck() {
const x = useMotionValue(0)
const y = useMotionValue(0)
const style = useTranslateStyle(x, y)
return <Motion.View style={[styles.puck, style]} />
}
SignatureReturns
useTranslateStyle(x: SharedValue<number>, y: SharedValue<number>)ReturnType<typeof useAnimatedStyle>

The style owns the whole transform key. Do not stack a second transform style beside it — transform is one key in React Native, so the later style replaces this one instead of merging. Nest another animated view, or build one style from x / y with useInterpolatedStyle, to add a transform.

applyBounds(value, min, max, elastic)

Clamp value to [min, max]. When elastic > 0 the overshoot past a bound is scaled by elastic instead of hard-clamped, which gives a rubber-band feel. min / max may be undefined to leave that side unbounded. It is a worklet, so it runs inside a gesture-handler pan handler on the UI thread and from a JS-thread PanResponder callback alike. useTouchDrag and useDrag apply their constraints and elastic options through it.

import { applyBounds } from '@rootnative/inertia'

applyBounds(120, -100, 100, 0) // 100
applyBounds(120, -100, 100, 0.5) // 110
applyBounds(50, undefined, 100, 0) // 50
SignatureReturns
applyBounds(value: number, min: number | undefined, max: number | undefined, elastic: number)number

useTouchDrag(options?)

PanResponder-backed drag hook. The keyboard-a11y and zero-extra-peer-dep counterpart to useDrag from @rootnative/inertia-gestures — both share the same onRelease shape, but useTouchDrag lives in core (@rootnative/inertia/touch) and returns panHandlers to spread on a Pressable / View instead of a gesture object for <GestureDetector>.

import { Motion } from '@rootnative/inertia'
import { useTouchDrag } from '@rootnative/inertia/touch'
import { Pressable } from 'react-native'

function Slider({ ticks }: { ticks: number[] }) {
const drag = useTouchDrag({
axis: 'x',
constraints: { left: 0, right: 280 },
onRelease: (e) => {
const snap = nearestTick(e.x, ticks)
return { x: { type: 'spring', to: snap, velocity: e.velocity.x } }
},
})

return (
<Pressable
accessibilityRole="adjustable"
onKeyDown={handleKey} // arrow keys compose alongside the drag
{...drag.panHandlers}
>
<Motion.View style={[styles.thumb, drag.animatedStyle]} />
</Pressable>
)
}
OptionTypeDefaultNotes
axis'x' | 'y' | 'both''both'Lock the gesture to one axis.
constraints{ left?, right?, top?, bottom? }noneBounds in px from the resting position. Each side independently optional.
elasticnumber (0–1)0Rubber-band coefficient past constraints. 0 hard-clamps.
onDragStart() => voidnoneFires on JS thread when drag begins.
onDragEnd(info: { x, y, velocity: { x, y } }) => voidnoneFires on JS thread when drag ends. Velocity normalized to px/sec.
onRelease(info) => ReleaseResult | voidnoneJS-thread release transition. Same shape as the gesture-handler useDrag's onRelease.

Returns { panHandlers, animatedStyle, dragX, dragY, isDragging }.

When to pick useTouchDrag over useDrag from @rootnative/inertia-gestures: you need keyboard a11y on the dragged element (arrow keys, PageUp / PageDown), OR you don't want to take react-native-gesture-handler as a peer dep. When to skip it: you're already using react-native-gesture-handler elsewhere — the gesture-handler useDrag's release velocity is UI-thread and slightly more precise; consistency across the app trumps a small per-call win.