Beta modeInstall commands on this page use lovdacn@beta, which installs from the beta registry and may change before the stable release.What’s in beta

Motion

Shared animation engine — the animate / activeAnimate contract and Motion hosts for animating any component or custom content.

Animations are in betaThe animate / activeAnimate API and the motion engine may change before the stable release. Install it with lovdacn@beta and re-check after upgrading. Feedback is welcome on GitHub.
Motion is in betaBeta features ship on the lovdacn@beta tag and can change before the stable release. Turn on Beta mode to read this page and to see beta install commands across the docs.

Motion is the shared animation engine behind lovdaCN's animate and activeAnimate props. It runs on the UI thread with Reanimated, respects system reduced-motion, and exposes typed object configuration plus standalone Motion hosts for your own content.

Beta. Object configuration (animate / activeAnimate) and the Motion hosts are available now. Utility-string syntax (e.g. animate="fade-in") and automatic per-component wiring across the whole registry are planned for later phases and may change. Install the CLI from the beta dist-tag while this stabilizes.

Installation

npx lovdacn@beta add motion

This copies components/ui/motion.tsx and installs react-native-reanimated and react-native-worklets. Follow Expo's Reanimated setup (Babel plugin) if your project does not already have it.

How it works: the primitives seam

Components don't import their hosts from react-native directly. They import them from components/ui/primitives:

import { Pressable, View, Text, TextInput } from '@/components/ui/primitives';

That one file has two interchangeable implementations:

  • plain (the default, installed with every component) — accepts animate / activeAnimate, ignores them, renders the raw React Native host. No Reanimated, so an app that never adds motion ships no animation runtime.
  • motion-aware (installed by lovdacn add motion) — renders engine-backed animated hosts.

Because every component renders through this seam, add motion swaps one file and the whole library starts honouring animate. Nothing in the components changes, and adding a new component costs no animation work.

lovdacn add button      → button + primitives (plain)   → no Reanimated
lovdacn add motion      → motion.tsx + primitives (motion-aware) → animation live everywhere

Component defaults live in the components, not the hosts — Button passes activeAnimate ?? 'press', so it has press feedback by default and you can override or disable it.

Usage

Button (reference integration)

Button supports both props out of the box and ships with a subtle default press scale.

import { Button } from '@/components/ui/button';

// Default: subtle press feedback (scale 0.97, snappy spring).
<Button onPress={save}>Save</Button>

// Custom mount + press animation.
<Button
  animate={{
    initial: { opacity: 0, translateY: 8 },
    to: { opacity: 1, translateY: 0 },
    transition: { type: 'timing', duration: 180, easing: 'ease-out' },
  }}
  activeAnimate={{
    to: { scale: 0.96 },
    transition: { type: 'spring', damping: 14, stiffness: 240 },
  }}
  onPress={save}
>
  Save
</Button>

// Disable only the default press feedback.
<Button activeAnimate={false} onPress={save}>No bounce</Button>

Motion host (any content)

Wrap user-owned content with Motion (or the typed MotionView, MotionPressable, MotionText, MotionTextInput hosts).

import { Motion } from '@/components/ui/motion';

<Motion
  animate={{ initial: { opacity: 0, scale: 0.96 }, to: { opacity: 1, scale: 1 } }}
  activeAnimate={{ scale: 0.98 }}
>
  <CustomTile />
</Motion>

Presets

Preset names resolve to the same object model.

<Motion animate="fade-in" />
<Motion animate="slide-up" />
<Button activeAnimate="press" />
<Motion animate="spin" /> {/* continuous; stops under reduced motion */}

Utility strings

Strings are sugar that compile to the same object config — there is no second engine.

<Button animate="fade-in slide-up duration-200" activeAnimate="press:scale-95 spring-snappy" />
<Motion animate="scale-95 rotate-45 delay-100" />

Supported tokens: presets (fade-in, slide-up, zoom-in, pop, spin, pulse, …), transforms (scale-*, opacity-*, translate-x-*, translate-y-*, rotate-*), transitions (duration-*, delay-*, ease-*, spring-soft|snappy|bouncy) and state prefixes (press:, hover:, focus:, checked:, selected:, open:, expanded:). Unknown tokens warn in development and are ignored.

asChild — animate someone else's element

Motion normally renders its own host. With asChild it merges the animated style, composed handlers and ref into a single child instead, so no extra layout node is added.

<Motion asChild animate="fade-in">
  <MyTile />          {/* must forward `ref` and accept `style` */}
</Motion>

Staggering a list

import { Motion, stagger } from '@/components/ui/motion';

{items.map((item, i) => (
  <Motion key={item.id} animate={stagger('slide-up', i, 60)}>
    <Card>{item.title}</Card>
  </Motion>
))}

API

Every visual host accepts SharedAnimationProps:

PropTypeDescription
animatefalse | MotionPresetName | string | AnimateConfigIdle / mount / loop animation. false disables it.
activeAnimatefalse | MotionPresetName | string | MotionTarget | ActiveAnimateConfigMotion while the component's semantic active state is true.
motionActivebooleanExplicit active-state override for static hosts (Card, Text, …).
reduceMotion"system" | "always" | "never"Reduced-motion policy. Defaults to system.

Semantics

  • animate defines the idle/lifecycle layer: initial is the first rendered state, to is the resting state.
  • activeAnimate overlays the idle target while the active state is on and returns to idle when it turns off.
  • State precedence: disableddraggingpress → semantic (checked, selected, open, expanded, current, loading) → focushover → idle.
  • animate={false} and activeAnimate={false} disable only their own layer.
  • User handlers (onPressIn, onPressOut, focus, hover, …) are composed, not replaced — each of your callbacks still fires exactly once. Refs resolve to the underlying host.

Reduced motion

With reduceMotion="system" (the default), motion snaps to its final accessible value instead of animating when the OS "Reduce Motion" setting is on, and continuous loops stop. Essential state (checked indicators, open content, focus affordances) is always preserved.

Class-driven animation on web (animate-in, animate-pulse, CSS transitions) is owned by CSS, not the engine, so lovdacn init also writes a @media (prefers-reduced-motion: reduce) block into your global stylesheet that neutralises it. One owner per property, both systems covered.

Beta scope

  • Available: object and utility-string animate / activeAnimate on every component that renders its host through the seam, Motion (including asChild), MotionView / MotionPressable / MotionText / MotionTextInput, useMotion, useMotionState, motionPresets, the shared durations / transitions tokens, stagger(), and reduced motion on both the engine and CSS paths.
  • Known gaps: Popover, Tooltip and HoverCard content renders inside the primitive's own animated wrapper, so it keeps its built-in enter/exit animation and does not expose animate yet — wrap those in <Motion> if you need custom motion. The API may still change before the stable release.