Animations
Swap out animation drivers per-platform or at runtime
Features
Animate any style prop with animation config per-prop.
Can animate across all states (media queries, hover, etc).
Four drivers you can swap out with type safety.
SSR safe mount animations.
Enter and exit animations with AnimatePresence.
Add animations to Tamagui with an animation driver. Animation drivers are designed to be swappable, so you can use lightweight CSS animations or other web-focused animation libraries on the web, while using larger but more advanced libraries like reanimated on native - all without having to change a line outside of configuration.
For this guide, we'll use the React Native driver as an example, but you can choose different animation drivers: CSS, React Native, Reanimated, or Motion. See the Choosing a Driver section below for a comparison.
Installation
yarn add @tamagui/animations-react-native
Then add it to your Tamagui config:
import { createAnimations } from '@tamagui/animations-react-native'import { createTamagui } from 'tamagui'export default createTamagui({animations: createAnimations({bouncy: {damping: 10,mass: 0.9,stiffness: 100,},lazy: {damping: 18,stiffness: 50,},quick: {damping: 20,mass: 1.2,stiffness: 250,},}),// ...})
Choosing a Driver
Tamagui supports four animation drivers, each with different strengths. The animation keys match between all drivers, so you can swap them out based on your platform needs.
Driver
Platform
Bundle Impact
Performance
Spring Physics
- CSS - Lightest bundle size, but lacks spring physics (easing curves only). Best for simple web-only apps.
- React Native - No extra bundle hit beyond React Native Web, but runs on-thread. Good for basic cross-platform animations.
- Reanimated - Wide support for both native and web with advanced features, but larger bundle size and slower web performance (though better than React Native driver).
- Motion - Off-thread performance via WAAPI with excellent spring physics, medium bundle hit. Note: if you're also using Reanimated, this creates duplicate bundle weight.
Platform-Specific Configuration
You can use different animation drivers per-platform using platform-specific file extensions. This allows you to optimize for each platform:
// animations.ts (web)import { createAnimations } from '@tamagui/animations-motion'export const animations = createAnimations({bouncy: {type: 'spring',damping: 10,stiffness: 100,},})
// animations.native.tsimport { createAnimations } from '@tamagui/animations-moti'export const animations = createAnimations({bouncy: {type: 'spring',damping: 10,stiffness: 100,},})
Then import without the extension:
// tamagui.config.tsimport { animations } from './animations' // Automatically picks .native.ts on nativeexport default createTamagui({animations,// ...})
Your bundler (Metro, Webpack, Vite) will automatically select the correct file based on the platform.
Optimizing Bundle Size
Experimental: Use our react-native-web-lite library to tree-shake animation drivers on web. Set useReactNativeWebLite: true in any Tamagui plugin configuration. This lets you completely remove React Native Web's animation system when using CSS or Motion drivers, saving significant bundle size.
// next.config.jsconst { withTamagui } = require('@tamagui/next-plugin')module.exports = withTamagui({config: './tamagui.config.ts',components: ['tamagui'],useReactNativeWebLite: true, // Experimental: tree-shake RNW on web})
Usage
The animation prop accepts the name of an animation you've configured. By default, animations will
apply to all animatable styles, similar to setting all in a CSS transition.
Here's an example animating hoverStyle:
The animation props rules
If you add an animation prop, you must always keep the prop. If you need the animation to be disabled,
pass false, null or even undefined if it suits you.
The spring-based animation drivers have expensive hooks that would degrade runtime performance if present on every component. As a workaround, the animation hooks are called conditionally on whether the animation key is present in the props object.
So, <Square animation={isActive ? 'bouncy' : null} /> rather than
<Square {...isActive && { animation: 'bouncy' }} />.
If you'd like to remove or add an animation prop after a component has already rendered, you'd have to change the key.
enterStyle
Setting enterStyle styles on any component tell it to start with those styles,
and after mount animate to their flat styles:
Granular animations
The animation prop accepts a string or a more complex object to customize
animations per-property.
The basic object we'll call an AnimationConfig, looks like this:
import { YStack } from 'tamagui'export default () => (<YStack animation={{ // only x and y will apply animations x: 'bouncy', y: { type: 'bouncy', overshootClamping: true, }, }} />)
Note that values can either map to AnimationKey as a string, or to
{ type: AnimationKey, ...configuration }
You can set a default value using a two-arity array with the default in the first position:
import { YStack } from 'tamagui'export default () => (<YStack animation={[ // all attributes get "bouncy" 'bouncy', // these are customized { y: 'slow', scale: { type: 'fast', repeat: 2, }, }, ]} />)
Delay
You can add a delay before animations start using the array syntax with a delay property (in milliseconds):
import { Square, XStack } from 'tamagui'export default () => (<XStack gap="$2">{[0, 1, 2, 3].map((i) => (<Square key={i} animation={['bouncy', { delay: i * 100 }]} enterStyle={{ opacity: 0, scale: 0.5, y: 20 }} size={50} bg="$color10" />))}</XStack>)
This creates a staggered animation effect where each square animates 100ms after the previous one. The delay works with all animation drivers and applies to enter, exit, and style change animations.
animateOnly
The animateOnly prop will limit your animation config to certain keys. It
accepts an array of strings that correspond to style property names.
AnimatePresence
exitStyle
AnimatePresence animates direct children before they unmount. It's inspired by and forked from Framer Motion , but works with any animation in Tamagui.
To use with @tamagui/core, install and import @tamagui/animate-presence.
It's already bundled and exported from tamagui.
You can use it simply with enterStyle + exitStyle:
import { AnimatePresence, View } from 'tamagui'export const MyComponent = ({ isVisible }) => (<AnimatePresence>{isVisible && (<View key="my-square" animation="bouncy" backgroundColor="green" size={50} enterStyle={{ opacity: 0, y: 10, scale: 0.9, }} exitStyle={{ opacity: 0, y: -10, scale: 0.9, }} />)}</AnimatePresence>)
Note you don't even need to set opacity on the base style. Tamagui knows to
normalize styles like opacity and scale to 1 (and y to 0) if it's not defined
on the base styles but is defined on enterStyle or exitStyle.
Wrap one or more tamagui components with AnimatePresence. This component will animate on enter and exit.
Animated child components must each have a unique key prop so AnimatePresence can track their presence in the tree.
The custom prop
custom

AnimatePresence also takes a custom property that allows you to update a
variant of the animated child before it runs it's exit animation. This is useful
for animating a child out of the screen before it unmounts in a different
direction, like the example above:
import { AnimatePresence } from '@tamagui/animate-presence'import { ArrowLeft, ArrowRight } from '@tamagui/lucide-icons'import { useState } from 'react'import { Button, Image, XStack, YStack, styled } from 'tamagui'const GalleryItem = styled(YStack, {zIndex: 1,x: 0,opacity: 1,fullscreen: true,variants: {// 1 = right, 0 = nowhere, -1 = leftgoing: {':number': (going) => ({enterStyle: {x: going > 0 ? 1000 : -1000,opacity: 0,},exitStyle: {zIndex: 0,x: going < 0 ? 1000 : -1000,opacity: 0,},}),},} as const,})const photos = ['https://picsum.photos/500/300','https://picsum.photos/501/300','https://picsum.photos/502/300',]const wrap = (min: number, max: number, v: number) => {const rangeSize = max - minreturn ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min}export function Demo() {const [[page, going], setPage] = useState([0, 0])const imageIndex = wrap(0, photos.length, page)const paginate = (going: number) => {setPage([page + going, going])}return (<XStack overflow="hidden" backgroundColor="#000" position="relative" height={300} width="100%" alignItems="center" ><AnimatePresence initial={false} custom={{ going }}><GalleryItem key={page} animation="slowest" going={going}><Image src={photos[imageIndex]} width={500} height={300} /></GalleryItem></AnimatePresence><Button accessibilityLabel="Carousel left" icon={ArrowLeft} size="$5" position="absolute" left="$4" circular elevate onPress={() => paginate(-1)} zi={100} /><Button accessibilityLabel="Carousel right" icon={ArrowRight} size="$5" position="absolute" right="$4" circular elevate onPress={() => paginate(1)} zi={100} /></XStack>)}
What to know when animating
Conditional animations and HMR
The animation hooks are heavy, which initially meant we either had to choose
great performance or animations. We've settled on a trade-off. We track if the
animation prop is set, and if so, we enable the hook. If it is ever set, even
just once, then the hooks will continue to run for the remainder of the
component lifecycle. This means if you ever plan to animate a component you
should keep animation always set on the component props. You can disable it
like so:
<View animation={condition ? 'animation-name' : null} />
Note that because of this constraint, you also will see an error if you add the
animation prop to a component in dev mode during an HMR. Often just saving once more will remove the error, or reloading at worst.