---
url: https://native-shell.com/modules/nsh-animated
description: "Native values, animation drivers, composed animations, easings and layout animation."
demo: https://native-shell.com/play/index.html#mod-animated
---

# nsh/animated

`nsh/animated` animates on the UI thread. You create a **native value**, bind it (or values derived from it) to a style property, and start an animation; from then on every frame is computed natively, without running JavaScript. A busy JS thread cannot make the animation stutter, and an animation costs no JS work per frame.

```ts
import { Animated, createNativeValue, Easing, LayoutAnimation, NativeNode, NativeValue } from "nsh/animated";
```

See [Animation](https://native-shell.com/docs/animation.md) for the guide.

## What a native value can drive

| Where | How |
|---|---|
| `opacity` | `style={{ opacity: value }}` |
| `backgroundColor` | `style={{ backgroundColor: value.interpolate({ inputRange: [0, 1], outputRange: ["#fff", "#000"] }) }}` |
| `transform` | `translateX`, `translateY`, `scale`, `scaleX`, `scaleY`, `rotate`, `rotateX`, `rotateY`, `rotateZ` steps: `transform: [{ translateX: value }]` |
| Scroll position | `<Scroll scrollValue={value}>` and `<List scrollValue={value}>` write the scroll offset into it each frame |
| Gestures | `pan={{ x, y }}`, `pinch={{ scale }}`, `rotation={{ angle }}` on any element: the gesture writes the values directly (see [Touch and gestures](https://native-shell.com/docs/touch-and-gestures.md)) |
| Canvas | Any number or color argument of a [canvas](https://native-shell.com/modules/nsh-canvas.md) drawing call |

Other style properties take plain values; to animate the layout itself (sizes, positions in the flow), use [`LayoutAnimation`](#layoutanimation).

## createNativeValue

```ts
function createNativeValue(value?: number): NativeValue;
```

Creates a native value starting at `value` (default `0`).

```tsx
import { mount, Text, View } from "nsh";
import { createNativeValue } from "nsh/animated";

function App() {
  const x = createNativeValue(0);
  let right = false;
  const toggle = () => {
    right = !right;
    x.spring(right ? 200 : 0);
  };
  return (
    <View style={{ flex: 1, padding: 24, paddingTop: 48, gap: 16 }}>
      <View style={{ width: 60, height: 60, borderRadius: 12, backgroundColor: "#6366f1", transform: [{ translateX: x }] }} />
      <Text onPress={toggle}>Move</Text>
    </View>
  );
}

mount(App);
```

> [!NOTE]
> JS cannot read a native value's current value: it lives on the UI thread and changes there every frame. Keep the target in a variable or a signal when your code needs it, as above. Gesture events report the values at their end (`PanEvent.valueX` / `valueY`).

## NativeValue

```ts
class NativeValue extends NativeNode {
  constructor(value: number);
  set(value: number): void;
  stop(): void;
  timing(to: number, options?: { duration?: number; easing?: Easing; delay?: number }): Promise<boolean>;
  spring(to: number, options?: { stiffness?: number; damping?: number; mass?: number; velocity?: number }): Promise<boolean>;
  decay(options?: { velocity?: number; deceleration?: number }): Promise<boolean>;
}
```

| Method | Description |
|---|---|
| `set(value)` | Jumps to `value`, stopping any running animation. |
| `stop()` | Stops the running animation where it is. |
| `timing(to, options)` | Animates to `to` along an easing curve. `duration` in ms (default 300), `easing` (default `"easeInOut"`), `delay` in ms (default 0). |
| `spring(to, options)` | Springs to `to`. `stiffness` (default 170), `damping` (default 26), `mass` (default 1), `velocity` (initial velocity, units per second). |
| `decay(options)` | Coasts from `velocity` (units per second) and slows down by `deceleration` per millisecond (default 0.998). Use it after a fling, with the velocity from `onPanEnd`. |

Each animation method returns a promise that resolves with `true` when the animation finished and `false` when it was stopped (by `stop`, `set` or another animation of the same value).

```ts
const opacity = createNativeValue(1);
const finished = await opacity.timing(0, { duration: 200 });
if (finished) visible.set(false);
```

## NativeNode

```ts
class NativeNode {
  interpolate(config: {
    inputRange: number[];
    outputRange: number[] | string[];
    extrapolate?: Extrapolation;
    extrapolateLeft?: Extrapolation;
    extrapolateRight?: Extrapolation;
    easing?: Easing;
  }): NativeNode;
  diffClamp(min: number, max: number): NativeNode;
  add(b: NativeNode | number): NativeNode;
  sub(b: NativeNode | number): NativeNode;
  mul(b: NativeNode | number): NativeNode;
  div(b: NativeNode | number): NativeNode;
  mod(b: NativeNode | number): NativeNode;
}

type Extrapolation = "extend" | "clamp" | "identity";
```

A node of the native animation graph: a `NativeValue`, or a value derived from others. Derived nodes are evaluated on the UI thread whenever their inputs change.

| Method | Description |
|---|---|
| `interpolate(config)` | Maps the input through piecewise ranges. `outputRange` holds numbers, colors (`"#ff0000"`, `"rgba(…)"`) or angles (`"45deg"`, `"1rad"`). Outside `inputRange` it clamps by default; `extrapolate` (or `extrapolateLeft` / `extrapolateRight`) can be `"extend"` (continue the line), `"clamp"` or `"identity"` (pass the input through). `easing` shapes each segment. |
| `diffClamp(min, max)` | Follows the input's changes but keeps the result within `min..max`: a header that hides while content scrolls up and comes back as soon as it scrolls down. |
| `add`, `sub`, `mul`, `div` | Arithmetic with another node or a constant. |
| `mod(b)` | Modulo, never negative for a positive `b` (keeping angles in 0..360). |

```tsx
const scroll = createNativeValue(0);
const header = scroll.diffClamp(0, 44);

<Scroll scrollValue={scroll} style={{ flex: 1 }}>{/* rows */}</Scroll>
<View style={{ height: 44, transform: [{ translateY: header.mul(-1) }] }} />
```

```ts
const progress = createNativeValue(0);
const rotate = progress.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] });
const color = progress.interpolate({ inputRange: [0, 0.5, 1], outputRange: ["#22c55e", "#eab308", "#ef4444"] });
```

## Animated

```ts
const Animated: {
  timing(value: NativeValue, config: { toValue: number; duration?: number; easing?: Easing; delay?: number }): CompositeAnimation;
  spring(value: NativeValue, config: { toValue: number; stiffness?: number; damping?: number; mass?: number; velocity?: number }): CompositeAnimation;
  decay(value: NativeValue, config?: { velocity?: number; deceleration?: number }): CompositeAnimation;
  sequence(list: CompositeAnimation[]): CompositeAnimation;
  parallel(list: CompositeAnimation[], config?: { stopTogether?: boolean }): CompositeAnimation;
  stagger(ms: number, list: CompositeAnimation[]): CompositeAnimation;
  delay(ms: number): CompositeAnimation;
  loop(animation: CompositeAnimation, config?: { iterations?: number; resetBeforeIteration?: boolean }): CompositeAnimation;
  add(a: NativeNode, b: NativeNode | number): NativeNode;
  subtract(a: NativeNode, b: NativeNode | number): NativeNode;
  multiply(a: NativeNode, b: NativeNode | number): NativeNode;
  divide(a: NativeNode, b: NativeNode | number): NativeNode;
  modulo(a: NativeNode, b: NativeNode | number): NativeNode;
  diffClamp(a: NativeNode, min: number, max: number): NativeNode;
};

interface CompositeAnimation {
  start(callback?: (result: { finished: boolean }) => void): Promise<boolean>;
  stop(): void;
  reset(): void;
}
```

`Animated` builds animations out of steps (`timing`, `spring`, `decay`, `sequence`, `parallel`, ...). Nothing runs until `start()`; then the whole tree runs natively, and each step starts on the UI thread at the exact moment the previous one ends (no frame gap, no JS round trip between steps).

| Function | Description |
|---|---|
| `timing`, `spring`, `decay` | One animation of one value; same options and defaults as the [`NativeValue`](#nativevalue) methods, with the target as `toValue`. |
| `sequence(list)` | Runs the animations one after another. |
| `parallel(list, { stopTogether })` | Runs them at the same time. With `stopTogether` (default `true`), stopping one stops all. |
| `stagger(ms, list)` | Runs them in parallel, each starting `ms` after the previous one. |
| `delay(ms)` | Waits; useful inside `sequence`. |
| `loop(animation, { iterations, resetBeforeIteration })` | Repeats `animation` `iterations` times (forever by default), putting its values back before each run unless `resetBeforeIteration` is `false`. |
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `diffClamp` | The same as the [`NativeNode`](#nativenode) methods, as functions. |

`CompositeAnimation`:

| Method | Description |
|---|---|
| `start(callback?)` | Runs it (again). Resolves, and calls `callback`, once for the whole tree with whether it finished rather than being stopped. |
| `stop()` | Stops it where it is. |
| `reset()` | Stops it and puts its values back where they were when it started. |

```ts
const ys = [0, 1, 2, 3].map(() => createNativeValue(0));
const hop = Animated.stagger(
  80,
  ys.map((y) =>
    Animated.sequence([
      Animated.timing(y, { toValue: -36, duration: 220, easing: Easing.out(Easing.quad) }),
      Animated.spring(y, { toValue: 0, stiffness: 300, damping: 12 }),
    ]),
  ),
);
hop.start(({ finished }) => console.log(finished ? "done" : "stopped"));
```

## Easing

```ts
type Easing =
  | "linear" | "ease" | "easeIn" | "easeOut" | "easeInOut"
  | [number, number, number, number]   // cubic Bézier x1, y1, x2, y2
  | EasingCurve;

const Easing: {
  linear: Easing;
  ease: Easing;
  bezier(x1: number, y1: number, x2: number, y2: number): Easing;
  in(e: Easing): Easing;
  out(e: Easing): Easing;
  inOut(e: Easing): Easing;
  quad: EasingCurve;
  cubic: EasingCurve;
  poly(n?: number): EasingCurve;
  sin: EasingCurve;
  circle: EasingCurve;
  exp: EasingCurve;
  elastic(bounciness?: number): EasingCurve;
  back(s?: number): EasingCurve;
  bounce: EasingCurve;
};
```

An easing is data, not a JS function: the runtime evaluates it natively. Wherever an `Easing` is accepted you can pass a name (`"easeOut"`), a cubic Bézier as four numbers, or a curve from the `Easing` object.

| Curve | Shape |
|---|---|
| `quad` | `t²` |
| `cubic` | `t³` |
| `poly(n)` | `tⁿ` (`n` defaults to 2) |
| `sin` | Sinusoidal |
| `circle` | Circular |
| `exp` | Exponential |
| `elastic(bounciness)` | Springy overshoot (`bounciness` defaults to 1) |
| `back(s)` | Pulls back before going; `s` is the overshoot (default 1.70158) |
| `bounce` | Bounces at the end |

The curves are in their "in" form (slow at the start). `Easing.out(curve)` reverses one, `Easing.inOut(curve)` makes it symmetric, `Easing.in(curve)` keeps it as is.

```ts
value.timing(1, { duration: 600, easing: Easing.out(Easing.back()) });
value.timing(1, { easing: Easing.bezier(0.2, 0, 0, 1) });
value.timing(1, { easing: "easeOut" });
```

## LayoutAnimation

```ts
const LayoutAnimation: {
  configureNext(config: LayoutAnimationConfig, onEnd?: () => void): void;
  create(duration: number, type?: LayoutAnimationType, property?: LayoutAnimationProperty): LayoutAnimationConfig;
  Types: { easeInEaseOut; spring; linear; easeIn; easeOut };
  Properties: { opacity; scaleXY };
  Presets: { easeInEaseOut: LayoutAnimationConfig; linear: LayoutAnimationConfig; spring: LayoutAnimationConfig };
  easeInEaseOut(onEnd?: () => void): void;
  linear(onEnd?: () => void): void;
  spring(onEnd?: () => void): void;
};

interface LayoutAnimationConfig {
  duration: number;                // ms
  create?: LayoutAnimationAnim;    // views inserted
  update?: LayoutAnimationAnim;    // views moved or resized
  delete?: LayoutAnimationAnim;    // views removed
}

interface LayoutAnimationAnim {
  type?: "easeInEaseOut" | "spring" | "linear" | "easeIn" | "easeOut";
  property?: "opacity" | "scaleXY" | "scale";
  duration?: number;
  delay?: number;
  springDamping?: number;          // 0..1, default 0.7
}
```

`LayoutAnimation.configureNext(config)` animates the layout changes of the next commit natively: views that move or resize glide to their new frames, new views appear (fading or scaling in, by `property`) and removed views disappear. Call it right before the state change that changes the layout.

| Member | Description |
|---|---|
| `configureNext(config, onEnd?)` | Animates the next commit's changes; `onEnd` runs when done. |
| `create(duration, type?, property?)` | A config using the same curve for all three phases. |
| `Presets.easeInEaseOut` | 300 ms, views fading in and out. |
| `Presets.linear` | 500 ms at constant speed. |
| `Presets.spring` | 700 ms; frames spring (damping 0.4), views fade linearly. |
| `easeInEaseOut(onEnd?)`, `linear(onEnd?)`, `spring(onEnd?)` | Shorthands for `configureNext(Presets.…)`. |
| `Types`, `Properties` | The names, as constants. |

```tsx
const open = signal(false);
const toggle = () => {
  LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
  open.update((o) => !o);
};

<View>
  <Text onPress={toggle}>Details</Text>
  <If when={open()}>
    <Text>Shown with a spring.</Text>
  </If>
</View>
```
