AnimatePresence

Animate components as they mount and unmount

AnimatePresence animates direct children before they unmount. It works with every animation driver in Tamagui.

It is inspired by and forked from Framer Motion.

To use with @tamagui/core, install and import @tamagui/animate-presence. It’s already bundled and exported from tamagui.

Basic usage

Use enter and exit object keys or string clauses to define how each property animates in and out:

import { AnimatePresence, html } from 'tamagui'
export const MyComponent = ({ isVisible }) => (
<AnimatePresence>
{isVisible && (
<html.div key="my-square" transition="bouncy" backgroundColor="green" size={50} opacity="enter:0 exit:0" y="enter:10px exit:-10px" scale="enter:0.9 exit: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 has an enter: or exit: clause.

Animated child components must each have a unique key prop so AnimatePresence can track their presence in the tree.

Enter/exit transitions

Set enter and exit transitions when mount and unmount should differ:

import { AnimatePresence, html } from 'tamagui'
export default ({ show }) => (
<AnimatePresence>
{show && (
<html.div key="panel" transition={{ enter: 'lazy', exit: 'quick' }} opacity="enter:0 exit:0" y="enter:20px exit:-20px" />
)}
</AnimatePresence>
)

enter and exit each take a whole transition value, so they accept a preset name, a CSS transition string, or an object. The rest of the transition is what property changes use while the element is mounted:

// enter slowly, exit quickly, property changes use bouncy
<html.div transition={{ preset: 'bouncy', enter: 'lazy', exit: 'quick' }} opacity="enter:0 exit:0" />

Because they are whole transition values, they can carry a delay and their own per-property entries:

// enter with lazy but move x slowly, exit with quick after 200ms
<html.div transition={{ preset: 'bouncy', enter: { preset: 'lazy', x: 'slow' }, exit: { preset: 'quick', delay: 200 }, }} opacity="enter:0 exit:0" x="enter:-100px exit:100px" />

An enter or exit value replaces the base while it applies rather than merging into it, the same way a later CSS transition declaration replaces an earlier one.

This works with all four animation drivers (CSS, React Native, Reanimated, Motion).

The custom prop

The custom prop updates a child’s variant before its exit animation runs, so it can leave in a different direction than it entered:

import { AnimatePresence } from '@tamagui/animate-presence'
import { ArrowLeft, ArrowRight } from './components/icons' // your generated icons, see Lucide Icons
import { useState } from 'react'
import { Button, Image, XStack, YStack, styled } from 'tamagui'
const GalleryItem = styled(YStack, {
zIndex: 1,
x: 0,
opacity: 1,
position: 'absolute',
inset: 0,
variants: {
// 1 = right, 0 = nowhere, -1 = left
going: styled.dynamic<number>((going) => ({
x: `0 enter:${going > 0 ? '1000px' : '-1000px'} exit:${ going < 0 ? '1000px' : '-1000px' }`,
opacity: '1 enter:0 exit:0',
zIndex: '1 exit: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 - min
return ((((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} transition="slowest" going={going}>
<Image src={photos[imageIndex]} width={500} height={300} />
</GalleryItem>
</AnimatePresence>
<Button aria-label="Carousel left" icon={ArrowLeft} size="5" position="absolute" left="4" circular boxShadow="0 0 10px rgba(0,0,0,0.5)" onPress={() => paginate(-1)} zIndex={100} />
<Button aria-label="Carousel right" icon={ArrowRight} size="5" position="absolute" right="4" circular boxShadow="0 0 10px rgba(0,0,0,0.5)" onPress={() => paginate(1)} zIndex={100} />
</XStack>
)
}

API reference

AnimatePresence Props

  • children: one or more Tamagui components with unique key props
  • initial: if false, children entering on initial mount will not animate (default: true)
  • custom: pass data to children’s variants for dynamic exit animations
  • mode: determines how entering and exiting children render. Use "wait" to render one child at a time.
  • onExitComplete: callback fired when all exiting children have finished animating

Component Props

When used inside AnimatePresence:

  • enter: clauses: values to animate from when mounting
  • exit: clauses: values to animate to when unmounting
  • transition: animation configuration (supports { enter, exit } syntax)
  • key: required unique identifier for AnimatePresence tracking

See also