code/tamagui.dev/data/docs/guides/how-to-upgrade.mdx
Tamagui v2 aligns with modern web standards, improves performance, and simplifies APIs. The migration is moderate -- mostly removing deprecated APIs, renaming props, and updating your config. This guide walks through every change.
Tamagui 2 requires:
Upgrade these first before proceeding.
Bump all @tamagui/* and tamagui packages to ^2.0.0:
# if using npm/yarn/pnpm, update all tamagui packages
npx tamagui check
@tamagui/animations-moti -- use @tamagui/animations-reanimated (same API)@tamagui/image-next -- use Image from tamagui directlyIn monorepos, you may want to add resolutions to avoid duplicate copies of core packages (critical for context/provider to work):
"resolutions": {
"@tamagui/core": "^2.0.0",
"@tamagui/web": "^2.0.0",
"tamagui": "^2.0.0"
}
You don't have to migrate to Config v5, and it changes some default style behaviors that may cause some issues. It's easier to migrate to Tamagui 2 first, then Config v5 after if you do choose to. Config v5 is mostly a superset of v5, but some media query names changed and some themes and colors are different.
// before
import { defaultConfig } from '@tamagui/config/v4'
// after
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css' // animations are now separate
Animations are no longer bundled with the config. Choose your driver:
@tamagui/config/v5-css -- CSS transitions (smallest bundle, web-only)@tamagui/config/v5-rn -- React Native Animated API@tamagui/config/v5-reanimated -- Reanimated (best native performance)@tamagui/config/v5-motion -- Motion (Web Animations API, experimental)import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations,
})
export type Conf = typeof config
declare module 'tamagui' {
interface TamaguiCustomConfig extends Conf {}
}
All root-level createTamagui settings have moved into the settings object:
// before (v1)
createTamagui({
defaultFont: 'body',
disableRootThemeClass: true,
// ...
})
// after (v2)
createTamagui({
...defaultConfig,
settings: {
...defaultConfig.settings,
// your overrides
},
})
Removed settings:
maxDarkLightNesting -- removed entirelycssStyleSeparator -- removed entirelythemeClassNameOnRoot -- handled by addThemeClassNamedisableRootThemeClass -- now part of settingsv5 changes two important defaults:
flexBasis changes from auto to 0 (React Native standard)position changes from relative to static (browser default)If your layout relies on the old behavior, restore it:
settings: {
...defaultConfig.settings,
styleCompat: 'legacy', // restores flexBasis: auto
defaultPosition: 'relative', // restores position: relative
}
Otherwise, you may need to add position="relative" explicitly on containers that have absolutely-positioned children, and add flexBasis="auto" where needed.
$2xl → $xxl$2xs → $xxs$max2Xl → $max-xxl$maxXl → $max-xl$maxLg → $max-lg$maxMd → $max-md$maxSm → $max-smMax queries changed from camelCase to kebab-case. New height-based queries are also available ($height-sm, $height-md, etc.) and a $pointerTouch query.
Breakpoint values now match Tailwind CSS: 640, 768, 1024, 1280, 1536.
Colors have been updated to Radix Colors v3 with slightly different values. Legacy colors are available at @tamagui/colors/legacy.
v5 adds new color themes: orange, pink, purple, teal, gray, neutral.
// before (v1) - manual theme-builder
import { createThemes, defaultComponentThemes } from '@tamagui/theme-builder'
const themes = createThemes({ ... })
// after (v2) - simplified
import { createV5Theme, defaultChildrenThemes } from '@tamagui/themes/v5'
const themes = createV5Theme({
childrenThemes: {
...defaultChildrenThemes,
// add custom color themes
cyan: { light: cyan, dark: cyanDark },
},
})
Component themes are off by default in v5. Use defaultProps in your config instead:
createTamagui({
...defaultConfig,
defaultProps: {
Button: { theme: 'accent' },
},
})
On the client, you can skip loading theme JS and hydrate from CSS variables instead:
themes: process.env.VITE_ENVIRONMENT === 'client'
? ({} as typeof themes)
: themes,
These are the most common find-and-replace changes.
animation to transition// before
<View animation="bouncy" />
<Sheet.Overlay animation="lazy" />
// after
<View transition="bouncy" />
<Sheet.Overlay transition="lazy" />
The TypeScript type also changed: AnimationProp to TransitionProp.
tag to render// before
<View tag="nav" />
<View tag="button" />
// after
<View render="nav" />
<View render="button" />
For tag="a", consider using the Anchor component instead.
Stack to View// before
import { Stack, type StackProps } from 'tamagui'
// after
import { View, type ViewProps } from 'tamagui'
themeInverse / <Theme inverse> to theme="accent"// before
<Button themeInverse>Primary</Button>
<Theme inverse><Card>...</Card></Theme>
// after
<Button theme="accent">Primary</Button>
<Theme name="accent"><Card>...</Card></Theme>
space / spaceDirection to gap// before
<YStack space="$4" spaceDirection="both">
// after
<YStack gap="$4">
onHoverIn / onHoverOut// before
<View onHoverIn={handler} onHoverOut={handler} />
// after
<View onPointerEnter={handler} onPointerLeave={handler} />
ellipse to numberOfLines// before
<Text ellipse>Long text...</Text>
// after
<Text numberOfLines={1}>Long text...</Text>
Replace React Native shadow props with CSS boxShadow:
// before
<View
shadowColor="$shadow3"
shadowRadius={20}
shadowOffset={{ height: 10, width: 0 }}
/>
// after
<View boxShadow="0 10px 20px $shadow3" />
The format is x y blur color. Multiple shadows are comma-separated. Spread and inset are also supported.
All React Native accessibility props are replaced with web-standard ARIA equivalents:
accessibilityLabel → aria-labelaccessibilityRole → roleaccessibilityHint → aria-describedbyaccessibilityState={{ disabled }} → aria-disabledaccessibilityState={{ selected }} → aria-selectedaccessibilityState={{ checked }} → aria-checkedaccessibilityState={{ busy }} → aria-busyaccessibilityState={{ expanded }} → aria-expandedaccessibilityValue → aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetextaccessibilityElementsHidden → aria-hiddenaccessibilityViewIsModal → aria-modalaccessibilityLiveRegion → aria-liveaccessible → tabIndex={0}focusable → tabIndexnativeID → idInput now uses web-standard HTML attributes as the primary API:
// before
<Input
keyboardType="email-address"
secureTextEntry
returnKeyType="send"
textContentType="emailAddress"
onChangeText={(text) => setText(text)}
onKeyPress={(e) => {
if (e.nativeEvent.key === 'Enter') submit()
}}
editable={false}
/>
// after
<Input
inputMode="email"
type="password"
enterKeyHint="send"
autoComplete="email"
onChange={(e) => setText(e.target?.value ?? e.nativeEvent?.text ?? '')}
onKeyDown={(e) => {
if (e.key === 'Enter') submit()
}}
readOnly
/>
The old React Native props still work but are deprecated.
Image now uses web-standard src instead of React Native's source:
// before
<Image
source={{ uri: 'https://example.com/photo.jpg', width: 200, height: 200 }}
resizeMode="cover"
/>
// after
<Image
src="https://example.com/photo.jpg"
width={200}
height={200}
objectFit="cover"
/>
fontFamily, fontSize, etc.) removed from the direct API -- style text through child componentstype="button" to prevent accidental form submissionsuseButton hook is deprecatedListItem.Text and ListItem.Subtitle child componentsspaceFlex, scaleSpace) removedactivationMode now defaults to 'manual' (was 'automatic') -- users must click or press Enter to change tabs, matching web standardsTabs.Trigger deprecated in favor of Tabs.TabGroup.Item wrapper is now required (no more auto-cloning direct children)space, separator, scrollable, showScrollIndicator, disablePassBorderRadius, forceUseItem<Separator /> manually between items when neededPopover.Sheet sub-components are replaced with standalone Sheet:
// before
<Adapt when="maxMd" platform="touch">
<Popover.Sheet modal dismissOnSnapToBottom>
<Popover.Sheet.Frame p="$4">
<Adapt.Contents />
</Popover.Sheet.Frame>
<Popover.Sheet.Overlay animation="quick" />
</Popover.Sheet>
</Adapt>
// after
<Adapt when="max-md" platform="touch">
<Sheet modal dismissOnSnapToBottom>
<Sheet.Frame p="$4">
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay transition="quick" />
</Sheet>
</Adapt>
SelectLabel is now SizableText-based instead of ListItem-based.
<Spacer /> removed from core -- import from @tamagui/spacercomposeEventHandlers -- compose manually: (val) => { a(val); b?.(val) }useTheme(props) -- use <Theme> component insteadThemeableStack -- use Viewbackgrounded prop -- use bg="$background"selectable prop -- use select="text"animatePresence prop (inline) -- wrap with <AnimatePresence> componentscrollbarWidth prop -- use CSSisWindowDefined -- use isBrowser from @tamagui/constantsuseTheme from @tamagui/next-theme -- renamed to useThemeSetting@tamagui/react-native-use-responder-events -- use pointer events (onPointerDown, etc.)v2 requires explicit setup imports for native features. Add these at the top of your app entry before any Tamagui imports. Note these are optional and mostly new, so only if you were using native gradient, or toast before do you need to do this.
// portals (Sheet, Dialog, Popover, Select, Toast)
import '@tamagui/native/setup-teleport'
// LinearGradient
import '@tamagui/native/setup-expo-linear-gradient'
// Toast (burnt)
import '@tamagui/native/setup-burnt'
// Menu (zeego)
import '@tamagui/native/setup-zeego'
// for smoother Sheet on native:
import '@tamagui/native/setup-gesture-handler'
With Expo Router, these setup imports must run before expo-router/entry. Create a custom entry point:
// index.js (at project root)
import '@tamagui/native/setup-zeego'
// add other setup imports here as needed
import 'expo-router/entry'
Then update your package.json:
{
"main": "index.js"
}
This ensures native modules like zeego are configured before Expo Router initializes your app.
Not much has changed, but there's some nice improvements you can make:
import { tamaguiAliases, tamaguiPlugin } from '@tamagui/vite-plugin'
export default {
plugins: [tamaguiPlugin()],
resolve: {
alias: [
...tamaguiAliases({
rnwLite: true, // use lightweight react-native-web
svg: true,
}),
],
},
}
Create a tamagui.build.ts at your project root:
import type { TamaguiBuildOptions } from 'tamagui'
export default {
components: ['tamagui'],
config: './src/tamagui.config.ts',
outputCSS: './src/tamagui.generated.css',
} satisfies TamaguiBuildOptions
No special Metro configuration is needed for Tamagui v2. A standard Expo Metro config works out of the box:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const config = getDefaultConfig(__dirname)
module.exports = config
Add resolveExtensions and a react-native-safe-area-context shim:
// next.config.js
module.exports = {
turbopack: {
resolveAlias: {
'react-native': 'react-native-web',
'react-native-svg': '@tamagui/react-native-svg',
'react-native-safe-area-context': './shims/react-native-safe-area-context.js',
},
resolveExtensions: [
'.web.tsx',
'.web.ts',
'.web.js',
'.web.jsx',
'.tsx',
'.ts',
'.js',
'.jsx',
'.json',
],
},
}
We recommend generating Tamagui CSS to tamagui.generated.css, just so it's easier to configure linters and such:
// before
import './tamagui.css'
// after
import './tamagui.generated.css'
Generate it with: npx tamagui generate-css --output ./src/tamagui.generated.css
These are the most common replacements you can do across your codebase. Run carefully and review each change:
animation= -> transition=
AnimationProp -> TransitionProp
AnimationKeys -> TransitionKeys
tag= -> render=
themeInverse -> theme="accent"
<Theme inverse -> <Theme name="accent"
<Stack -> <View
</Stack> -> </View>
StackProps -> ViewProps
onHoverIn= -> onPointerEnter=
onHoverOut= -> onPointerLeave=
$2xl= -> $xxl=
$2xs= -> $xxs=
maxMD -> max-md
maxLG -> max-lg
maxSM -> max-sm
maxXL -> max-xl
max2Xl -> max-xxl
While not required for migration, these v2 features can improve your app:
boxShadow -- full CSS box-shadow support with tokensbackgroundImage -- CSS gradients with token supportfilter, mixBlendMode -- graphical effectsscope prop on Dialog, Popover, Sheet, Tooltip -- mount once at root for performanceactiveStyle / activeTheme on Switch, Checkbox, ToggleGroup, TabsanimatedBy proptransition delay and enter/exit control -- transition={{ enter: 'lazy', exit: 'quick' }}@tamagui/switch-headless, @tamagui/checkbox-headless, etc.tamagui build, tamagui generate, tamagui generate-prompt, tamagui check@tamagui/* packages to ^2.0.0@tamagui/core, @tamagui/web, tamagui@tamagui/config/v4 to @tamagui/config/v5@tamagui/config/v5-css or other driver)declare module 'tamagui' type augmentationsettings objectanimation prop to transition everywheretag prop to renderStack with View, StackProps with ViewPropsthemeInverse / <Theme inverse> with theme="accent"space / spaceDirection with gaponHoverIn / onHoverOut with pointer eventsboxShadow$2xl to $xxl, max queries to kebab-case)source to srcGroup.ItemactivationMode default changed, Trigger to Tab)Popover.Sheet.* with standalone Sheet.*ToastProvider wrapper to Toaster siblingSpacer from @tamagui/spacer if used@tamagui/animations-moti with @tamagui/animations-reanimatedposition="relative" where needed)color-mix())npx tamagui check to verify dependency consistency