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).
Three 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. See the configuration docs for more on how to set it up, and how to set up different animation drivers per-platform.
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.
Installation
CSS
The @tamagui/animations-css
package works with the tamagui compiler and
runtime to give you simple ways to share typed animations across all your
components.
To install it add to your package.json:
yarn add @tamagui/animations-css
Then add it to your config:
import { createAnimations } from '@tamagui/animations-css'import { createTamagui } from 'tamagui'export default createTamagui({animations: createAnimations({fast: 'ease-in 150ms',medium: 'ease-in 300ms',slow: 'ease-in 450ms',}),// ...})
At runtime, the plugin does very little except to set the transition
property
in CSS. At compile-time, the compiler does the same, ensuring you get all the
benefits of prop removal and view flattening even when using animations.
React Native Animated
React Native's Animated library is the animation library that comes built into React Native and React Native Web.
To install it add to your package.json:
yarn add @tamagui/animations-react-native
Then add it to your config:
import { createAnimations } from '@tamagui/animations-react-native'import { createTamagui } from 'tamagui'export default createTamagui({animations: createAnimations({fast: {damping: 20,mass: 1.2,stiffness: 250,},medium: {damping: 10,mass: 0.9,stiffness: 100,},slow: {damping: 20,stiffness: 60,},}),// ...})
Reanimated
Reanimated is an animation library that targets React Native and React Native Web. It runs off-thread animations, and provides nicer syntax and hooks. It weighs over 100Kb minified though, which makes it not always useful for the web.
If using the starter, it configures excludeReactNativeWebExports
to omit
FlatList and other views reanimated expects, change that config to fix errors.
To install it add to your package.json. We use Moti as the driver, which is a very light layer above reanimated:
yarn add @tamagui/animations-moti react-native-reanimated
Then add it to your config:
import { createAnimations } from '@tamagui/animations-moti'import { createTamagui } from 'tamagui'export default createTamagui({animations: createAnimations({fast: {type: 'spring',damping: 20,mass: 1.2,stiffness: 250,},medium: {type: 'spring',damping: 10,mass: 0.9,stiffness: 100,},slow: {type: 'spring',damping: 20,stiffness: 60,},}),// ...})
At runtime, this plugin parses animatable style properties and hands them over to reanimated off-thread, using worklets. It doesn't do anything at compile time - reanimated must run via JS.
Note the keys match between CSS and reanimated, so you can swap them out.
Save bundle size by removing Animated
and AnimatedFlatList
from
react-native-web. On Next.js, use the excludeReactNativeWebExports`
option config option.
Usage
The animation
can now accept slow
as a value. By default, animations will
apply to all animatable styles, a lot like setting all
in a CSS transition
.
Let's test this by setting hoverStyle
:
Here's an example animating hoverStyle
:
A note on usage
As of beta 150, we're adding a rule for an edge case: any animated component
that is removing or adding an animation at any time, keep the animation
prop
defined.
So, <Square animation={isActive ? 'bouncy' : null} />
rather than
<Square {...isActive && { animation: 'bouncy' }} />
. If you do absolutely need
the latter, you must change the key
at the same time.
Why? The animation hooks are heavy and would otherwise have to run on every component. Plus enter/exit animations further add renders. Tamagui is designed for performance in the happy path. If every component you render had to run all the animation logic, a lot of the performance of Tamagui would be nullified.
enterStyle
Setting enterStyle
styles on any component tell it to start with those styles,
and immediately transition into their regular 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, }, }, ]} />)
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
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://placekitten.com/500/300','https://placekitten.com/501/300','https://placekitten.com/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, images.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 source={{ uri: 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 screen, or reloading at worst.
SSR safe enter animations for web
Tamagui works hard to make all the animations drivers SSR safe. While the CSS driver is relatively easy, the two others: reanimated, and React Native Web, both require some tricky logic to ensure that they don't flicker, don't hide content for users without JavaScript turned on, and still animation properly on initial render.
The short story is that if you want SSR safe animations, add this somewhere in your head
tag:
<head><script dangerouslySetInnerHTML={{ // avoid flash of entered elements before enter animations run: __html: `document.documentElement.classList.add('t_unmounted')`, }} /></head>
Perfect SSR
To achieve the ideal behavior for SSR with a spring driver, Tamagui does the following:
- On server, render using CSS only and with all
enterStyle
off, this way you can use CSS media queries and CSS variables which are a must for the initial render to match on hydration. - During that first server render, gather
enterStyle
as atomic CSS with the.t_unmounted
selector prefixed. - Add the code snippet above to your head, which will ensure the t_unmounted is in place before the browser shows anything visually.
- On first client render, you have to match the exact server render, so you render once with CSS.
- Render once more, this time swapping out for the animated spring component, with enterStyle enabled so it matches the initial render.
- Gather your styles once more with enterStyle turned off, and send the spring drivers happily off to do their bouncy things.
Previous
Themes
Next
Theme