We’re live on Product Hunt — click here to upvote

How We Handle Animations and Gestures in AI-Generated Apps

(154 chars):

RI

By Riya

20th Jul 2026

Last updated: 20th Jul 2026

How We Handle Animations and Gestures in AI-Generated Apps

Most AI code generators produce screens that look right in a screenshot and feel dead in your hand. Buttons don't respond to a press. Lists snap into place instead of settling. Modals appear like a jump cut. The static UI is fine — the motion layer is missing.

That's a solvable problem. Animation and gesture code in React Native is boilerplate-heavy but pattern-rich, which makes it a great fit for a language model — if the model is instructed correctly, has the right libraries pre-installed, and can preview the result before the user sees it. This post walks through how RapidNative generates React Native animations end to end: the animation stack we ship, the system-prompt rules that tell our AI when to reach for motion, the browser-side shim that runs react-native-reanimated inside a live preview iframe, and the guardrails that keep haptics from being sprinkled on every tap.

Mobile app animation preview on a smartphone screen Motion is the difference between a screenshot and an app — Photo by William Iven on Unsplash

The Animation Stack We Ship in Every Generated App

Every project RapidNative generates ships with the same motion primitives pre-installed in the scaffold, so the AI never has to guess whether a library is available or negotiate a version. The relevant lines from our fullstack template's package.json:

"react-native-gesture-handler": "2.28.0",
"react-native-reanimated": "4.1.3",
"@legendapp/motion": "2.4.0",
"expo-haptics": "…",
"expo-blur": "…"

Three of those pull their weight in almost every generated app:

  • react-native-reanimated 4.1.3 — the workhorse. Reanimated 4 gives us shared values, worklets, and the entering/exiting animation presets that make list items stagger without a single line of imperative animation code.
  • react-native-gesture-handler 2.28.0 — swipe-to-dismiss rows, drag-to-reorder lists, pull-down sheets, pinch-to-zoom. The library exposes a declarative gesture composition API that's much easier for an LLM to write correctly than the older PanResponder pattern.
  • @legendapp/motion — a higher-level layout-animation helper that's useful when the model wants "animate this container's size and children when items are added" without hand-rolling LayoutAnimation calls.

The Reanimated version in the live preview is one patch behind the native scaffold (4.1.0 vs 4.1.3) because our browser bundler pins to a version whose worklet handling we've validated against a JavaScript-runtime shim. On device, the app ships with the newer patch. On the preview, the API surface is identical, but the animations run through a requestAnimationFrame loop instead of the native worklet runtime — more on that in a moment.

Ship-day payoff: when a user asks the AI for "a fitness tracker with a pulsing streak badge and a swipe-to-log habit list," we're not generating install instructions or version negotiations. The libraries are already there. The AI just writes the code.

Teaching the AI When to Reach for Motion (The "Polish Layer")

Motion in an app is a judgment call, not a checklist. The wrong animation makes an app feel gimmicky; the right one makes it feel expensive. We handle that judgment in the system prompt itself. Here's the actual instruction block our fullstack template feeds the model on every screen generation:

A technically-correct screen is 6/10. An App Store Editor's Choice screen has a polish layer on top: typography that carries the mood, motion that guides the eye, tactile feedback on key taps, transitioning images, depth from blur. Use Expo's full ecosystem to add this layer.

Then the four motion-specific bullets:

  1. Entrance motion — Reanimated's entering/exiting presets. Stagger list items on render, animate heroes on mount, transition between states. The prompt explicitly warns the model not to use the same preset across every app — brutalist styles get punchy snaps, glass gets soft fades, editorial stays still, maximalist gets bold motion.
  2. Tactile feedbackexpo-haptics. Marked as opt-in, not a default. The prompt is blunt about this: "Do NOT sprinkle haptics on every Pressable / list tap / tab change / toggle."
  3. Animated values — Reanimated shared values for count-up dashboard metrics, pulsing streak badges, scale on focus, parallax on scroll.
  4. Gestures — Gesture Handler for swipe-to-dismiss, pull-down sheets, drag-to-reorder. Already in the scaffold.

The AI also gets a polish checklist it evaluates against on new-screen generations:

- [ ] List items have entering animations (preset chosen to match the style)
- [ ] Primary CTAs trigger haptic feedback on press
- [ ] Modals / overlays / sticky headers use blur (when the style allows)
- [ ] Dashboard metrics animate on mount (count-up, scale-in, fade)
- [ ] Every Pressable has a press-feedback effect (scale, opacity, or style-appropriate variant)

That checklist isn't decoration — it's how we get consistent polish across thousands of generated apps without giving every app the same identical motion signature. The animation is matched to the visual style the AI has already chosen for the app.

Designer choosing motion patterns for an app Design-matched motion — the wrong animation makes an app feel gimmicky, the right one makes it feel expensive — Photo by Balázs Kétyi on Unsplash

The Browser Preview Problem: Running Reanimated Without a Native Runtime

Here's the constraint that makes AI-generated React Native animations genuinely hard: the user is watching a live preview in a browser tab, not on a phone. Reanimated is a native library. Its whole reason for existing is to run animations on the native UI thread via C++/JVM worklets, bypassing the JavaScript bridge. Worklets don't work in a browser. useSharedValue needs a shared-value host. Spring physics on the UI thread require a native runtime that doesn't exist in Chrome.

Our preview pipeline solves this with a two-part trick:

Part one — the bundler. Every project is bundled in the browser by a Web Worker running almostmetro, our incremental bundler. When file changes come in from the editor, the worker rebundles and posts the compiled code back to the main thread over postMessage. The main thread injects that code into a preview iframe that runs react-native-web. No round-trip to a server, no Expo Snack, no Snack sandbox proxy — the entire preview is local to the browser tab.

Part two — the Reanimated shim. We inject a JavaScript-runtime shim that polyfills Reanimated's API for react-native-web. That shim lives in src/modules/file/bundler-shims.ts and implements the full Reanimated 4 surface that the AI is allowed to use:

withTiming, withSpring, withDecay, withDelay,
withRepeat, withSequence, cancelAnimation

Spring physics, damping, stiffness, and mass are all reimplemented on top of requestAnimationFrame. Easing gets a full library: linear, ease, quad, cubic, poly, sin, circle, exp, elastic, back, bounce, bezier, plus the in/out/inOut modifiers. useSharedValue becomes a plain object with listeners; useAnimatedStyle re-runs on shared-value changes and applies the resulting style to a DOM element via react-native-web.

The animated component set covers the primitives Reanimated exposes: Animated.View, Animated.Text, Animated.Image, Animated.ScrollView, Animated.FlatList, plus a createAnimatedComponent() factory for anything else.

The entering/exiting preset library — the thing that makes the polish checklist practical — is fully implemented too:

FadeIn, FadeOut, FadeInRight/Left/Up/Down,
SlideInRight/Left/Up/Down, SlideOutRight/Left/Up/Down,
ZoomIn, ZoomOut, BounceIn, BounceOut,
FlipInXUp/XDown/YLeft/YRight, StretchInX/Y,
RotateInDownLeft/DownRight/UpLeft/UpRight,
LightSpeedInRight/Left, PinwheelIn, RollInLeft/Right

Roughly forty presets, each mapped to a keyframe animation on the web target. What the user sees in the preview iframe is what the AI wrote in Animated.View entering={FadeInDown.duration(400)} — animated with real spring physics, not a static screenshot with a "preview coming soon" placeholder.

The tradeoff is honest: the preview runs on requestAnimationFrame, not the native UI thread. Complex animations that would hit 60fps on device might drop frames on a low-end laptop. But the visual and behavioral correctness is preserved, and when the user exports the project to their phone via a QR code from expo-router, the same code runs on the real Reanimated worklet runtime and unlocks the full 60fps promise.

Gestures: A Different Correctness Problem

Gesture code has a different challenge than animation code. Animation code is mostly declarative — you describe the state, and Reanimated interpolates it. Gesture code is stateful and event-driven. A swipe-to-dismiss handler needs to track finger position, distinguish a horizontal swipe from a vertical scroll, resist the gesture until a threshold, and either snap back or animate off-screen when released.

The gesture library that ships with our scaffold — react-native-gesture-handler 2.28.0 — makes this dramatically easier than it used to be. The Gesture.Pan(), Gesture.Tap(), Gesture.LongPress(), and Gesture.Simultaneous(...) composers are declarative and LLM-friendly.

But there's a second correctness problem specific to AI-generated code: imports. LLMs are famously bad at getting import statements right. They hallucinate exports, forget to import symbols they use, or import from a subpackage that doesn't exist. For animations and gestures, we solved this with a JSX import map that auto-injects imports for known symbols. The relevant lines from src/shared/utils/jsxImportMap.ts:

Animated:                { source: 'react-native-reanimated',       type: 'default' },
GestureDetector:         { source: 'react-native-gesture-handler',  type: 'named'   },
GestureHandlerRootView:  { source: 'react-native-gesture-handler',  type: 'named'   },
Swipeable:               { source: 'react-native-gesture-handler',  type: 'named'   },
LottieView:              { source: 'lottie-react-native',           type: 'default' },

If the AI generates code that uses Animated.View or <GestureDetector> but forgets the import, our post-processing step adds it. This means the AI can focus on writing the animation logic — the part that matters — instead of getting bogged down in import bookkeeping. It also means the model can't create a broken import to a symbol that doesn't exist in the library, because the map is the source of truth for what's valid.

For gesture code specifically, this pays off constantly. The composed-gesture pattern uses GestureHandlerRootView at the app root and GestureDetector at the interaction site. That's two named imports from react-native-gesture-handler that the model would otherwise forget on 20% of generations. Now it forgets zero of them.

Person interacting with a smartphone touchscreen Gesture handling is stateful and event-driven — a different correctness problem than animation — Photo by cottonbro studio on Pexels

Why Haptics Are Opt-In (And Motion Is Not)

The single most opinionated rule in our animation prompt is that expo-haptics is off by default. The rationale, straight from the prompt:

Overuse burns battery, feels cheap, and trains users to ignore haptics. Do NOT sprinkle haptics on every Pressable / list tap / tab change / toggle. Only reach for it when (a) the user explicitly asks for haptic feedback, or (b) the app's core interaction genuinely benefits from it — a timer hitting zero, long-press confirmation, swipe-to-delete confirmation, pull-to-refresh snap.

This is a deliberate asymmetry. Entrance animations, animated values, and gestures are enabled by default because getting them wrong is cheap — a slightly-off fade-in is invisible. Getting haptics wrong is expensive — a phone that buzzes on every tap gets muted, and once a user mutes haptics they stay muted. So we bias the model toward always animating and rarely haptic-ing.

The same logic governs expo-blur: it's available, but the prompt teaches the AI that blur is a style-dependent choice (great for glass and premium styles, wrong for brutalist or editorial styles), not a universal default.

This kind of instruction is the meta-level lesson of building an AI app generator: what you don't tell the model to do matters as much as what you do.

Feature vs Polish Gating: Don't Add Motion on Bugfixes

There's one more rule that turned out to be surprisingly load-bearing. If a user says "the tab bar is misaligned, fix it," the AI must NOT install expo-haptics, add entrance animations to the tab bar, or import expo-blur. Bugfixes and content updates should touch as little code as possible. Only new-screen and new-feature generations are allowed to add polish-layer imports.

We enforce this with an explicit prompt-level gate:

OPT-IN POLISH CHECKexpo-haptics, new Google Fonts, react-native-reanimated entrance animations, and expo-blur are OPT-IN polish. If the user's request is a feature/content/bug task, the response MUST NOT install any of these packages or import from them.

This one rule prevents a common failure mode of AI code generators: the "sprawling refactor" where a one-line bug turns into a 400-line PR that touches unrelated files. Motion belongs to the initial generation and to explicit new-feature requests. Bugfixes stay boring. That gate is what makes iterative editing feel safe.

Putting It All Together: What a Generated Screen Actually Looks Like

Here's what these rules produce in practice — a stripped-down version of the kind of list screen the AI generates when a user asks for "a habit tracker with a swipe-to-complete row and a fade-in list":

import { Pressable, Text, View } from 'react-native';
import { FadeInDown } from 'react-native-reanimated';
import { Swipeable } from 'react-native-gesture-handler';

export function HabitList({ habits, onComplete }) {
  return (
    <View className="flex-1 p-4">
      {habits.map((habit, i) => (
        <Animated.View
          key={habit.id}
          entering={FadeInDown.delay(i * 60).duration(320)}
        >
          <Swipeable
            renderRightActions={() => (
              <Pressable onPress={() => onComplete(habit.id)}>
                <Text>Done</Text>
              </Pressable>
            )}
          >
            <View className="p-4 bg-card rounded-2xl">
              <Text className="text-foreground">{habit.name}</Text>
            </View>
          </Swipeable>
        </Animated.View>
      ))}
    </View>
  );
}

Notice what's not there. There's no haptic call — the swipe animation is the feedback. There's no repeated animation preset on every element — only the row containers stagger in. The entering prop uses Reanimated's declarative preset API, so there's no shared value to manage, no useEffect to run, no animation cleanup to remember. The gesture handler is Swipeable, the pre-built high-level primitive, not a hand-rolled Gesture.Pan() composition — because the AI knows that hand-rolling a swipe row means getting resistance curves and dismiss thresholds right, and that's a place where mistakes are visible.

That's five lines of animation-and-gesture code, generated correctly the first time, previewing in the browser the second the AI finishes streaming, and running at 60fps when the user scans the QR code and opens the app on their phone.

Why This Matters for Anyone Using an AI App Builder

Most AI code generators optimize for "does the code compile." That's a low bar. The real bar for a mobile app is "does it feel like an app my users would keep on their phone." The gap between those two bars is almost entirely polish — motion, gestures, tactile feedback, blur, transitions. Getting that gap closed at generation time (rather than during a manual "add animations later" pass that never happens) is the difference between an AI-generated app that ships and one that gets abandoned in an editor tab.

If you want to see this in action, start a new project on RapidNative and ask for something with motion in the prompt — a fitness dashboard, a swipe-based reader, a habit tracker, a photo gallery. The animations run in the preview immediately. When you export the project and open it on your phone via the Expo QR code, those same animations run natively.

For a deeper look at other parts of the pipeline, we've written about how RapidNative delivers real-time React Native live preview across every device, why we run a 4-step LLM pipeline to generate React Native code, and the React Native performance optimization playbook — animations are one thread in a much bigger story.

Start now

Ready to build your app?

Turn your idea into a production-ready React Native app in minutes.

Free tools to get you started

Questions

Frequently asked questions

What is RapidNative?

RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.

Can I export the code?

Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.

Is RapidNative free to use?

Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.

Do I need to know how to code?

No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.

How long does it take to build an app?

Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.