code/tamagui.dev/data/docs/core/config-v5.mdx
V5 builds on v4 with more colors, theme helpers, and expanded media queries. Migration from v4 is straightforward - see what's changed.
First install the config package:
npm install @tamagui/config
v5-css, v5-rn,
v5-reanimated, or v5-motion. See
Choosing an Animation Driver.2xl/2xs → xxl/xxs/xxxs. Max queries use
kebab-case: max2Xl → max-xxl.styleCompat: 'react-native' - flex now uses flexBasis: 0 by default
(v4 used 'legacy' with flexBasis: auto).defaultPosition not set - Defaults to browser static instead of v4's
'relative'. You can still set it in your config.shadow1-shadow8, highlight1-highlight8,
background01/background001 through background08, color01/color001,
white0/white02-white08, black0/black02-black08, and more0ms to 500ms, plus named
springs like quick, bouncy, lazy, and variants like quickLessBouncycreateV5Theme helper for easy theme customization and swapping colorsgetTheme callback in createV5Theme for computing custom values across
every theme automatically based on each theme's palette and schemechildrenThemes you want to
createV5Theme to replace or extend the default color setheight-sm, height-md, height-lg and max variantstouch and hoverable media queries for device capability detectionadjustPalette, adjustPalettes for transforming
palettesTo restore v4 behavior while using v5:
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations,
settings: {
...defaultConfig.settings,
styleCompat: 'legacy',
defaultPosition: 'relative',
},
})
Update media query names in your code: $2xl → $xxl, $2xs → $xxs,
$max2Xl → $max-xxl.
V5 provides separate entry points for different animation drivers, so you only bundle what you need:
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations,
})
Available animation drivers:
@tamagui/config/v5-css - CSS animations (smallest bundle, great for web)@tamagui/config/v5-motion - Motion animations (spring physics, smooth)@tamagui/config/v5-rn - React Native Animated API@tamagui/config/v5-reanimated - Reanimated (best native performance)For apps targeting both web and native, you can use different animation drivers per platform:
import { defaultConfig } from '@tamagui/config/v5'
import { animations as animationsCSS } from '@tamagui/config/v5-css'
import { animations as animationsReanimated } from '@tamagui/config/v5-reanimated'
import { createTamagui, isWeb } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations: isWeb ? animationsCSS : animationsReanimated,
})
This gives you CSS animations on web (smaller bundle, better performance) and Reanimated on native (smooth 60fps animations).
All v5 animation drivers include these preset animations:
Timing animations (fixed duration):
<PropsTable data={[ { name: '0ms', description: 'Instant (0ms)' }, { name: '50ms', description: '50 milliseconds' }, { name: '75ms', description: '75 milliseconds' }, { name: '100ms', description: '100 milliseconds' }, { name: '200ms', description: '200 milliseconds' }, { name: '250ms', description: '250 milliseconds' }, { name: '300ms', description: '300 milliseconds' }, { name: '400ms', description: '400 milliseconds' }, { name: '500ms', description: '500 milliseconds' }, ]} />
Spring animations (physics-based):
<PropsTable data={[ { name: 'superBouncy', description: 'Very bouncy, playful spring' }, { name: 'bouncy', description: 'Bouncy spring with natural feel' }, { name: 'superLazy', description: 'Very slow, heavy spring' }, { name: 'lazy', description: 'Slow, relaxed spring' }, { name: 'medium', description: 'Balanced spring for general use' }, { name: 'slowest', description: 'Slowest spring animation' }, { name: 'slow', description: 'Slow spring animation' }, { name: 'quick', description: 'Fast, responsive spring' }, { name: 'quickLessBouncy', description: 'Quick with minimal overshoot' }, { name: 'quicker', description: 'Faster spring' }, { name: 'quickerLessBouncy', description: 'Faster with minimal overshoot' }, { name: 'quickest', description: 'Fastest spring' }, { name: 'quickestLessBouncy', description: 'Fastest with minimal overshoot', }, ]} />
The createV5Theme function lets you customize the default v5 themes. The
easiest way to add or change colors is using @tamagui/colors which provides
Radix color palettes:
import { createV5Theme, defaultChildrenThemes, defaultConfig } from '@tamagui/config/v5'
import { cyan, cyanDark, amber, amberDark } from '@tamagui/colors'
import { createTamagui } from 'tamagui'
const themes = createV5Theme({
childrenThemes: {
// include defaults (blue, red, green, yellow, etc.)
...defaultChildrenThemes,
// add new colors
cyan: { light: cyan, dark: cyanDark },
amber: { light: amber, dark: amberDark },
},
})
export const config = createTamagui({
...defaultConfig,
themes,
})
Or use only the colors you need:
import { createV5Theme } from '@tamagui/config/v5'
import { blue, blueDark, gray, grayDark } from '@tamagui/colors'
// minimal theme with just blue and gray
const themes = createV5Theme({
childrenThemes: {
blue: { light: blue, dark: blueDark },
gray: { light: gray, dark: grayDark },
},
})
<PropsTable data={[ { name: 'childrenThemes', type: 'Record<string, { light: ColorObject, dark: ColorObject }>', description: 'Color themes to include. Use defaultChildrenThemes to include defaults, or pass only the colors you need. Works directly with @tamagui/colors.', }, { name: 'darkPalette', type: 'string[]', description: 'Override the dark base palette (12 colors from darkest to lightest)', }, { name: 'lightPalette', type: 'string[]', description: 'Override the light base palette (12 colors from lightest to darkest)', }, { name: 'grandChildrenThemes', type: 'Record<string, { template: string }>', description: 'Override grandChildren themes (alt1, alt2, surface1, etc.)', }, { name: 'componentThemes', type: 'false | ComponentThemes', default: 'false', description: 'Component themes (defaults to false in v5). We recommend using defaultProps instead.', }, { name: 'getTheme', type: '(info: { palette: string[], scheme: "light" | "dark" }) => Record<string, string>', description: "Callback that runs for every generated theme, letting you compute custom values based on each theme's palette and color scheme. The default uses this to generate opacity variants like color01, background001, etc.", }, ]} />
The getTheme option lets you add computed colors to every generated theme. It
receives each theme's palette and scheme, so your custom values automatically
adapt to every color and light/dark variant:
import { createV5Theme, opacify } from '@tamagui/config/v5'
const themes = createV5Theme({
getTheme: ({ palette, scheme }) => {
const bg = palette[7]
const fg = palette[palette.length - 2]
return {
// add your own computed theme values
myOverlay: opacify(bg, 0.5),
mySubtleText: opacify(fg, 0.6),
}
},
})
This is how v5 generates its built-in opacity tokens (background01,
color001, etc.) — your custom getTheme merges on top of the defaults.
For a more muted look, use the subtle variant which has desaturated colors:
import { defaultConfig, themes } from '@tamagui/config/v5-subtle'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
themes,
})
Use adjustPalette to transform colors with a callback function. The callback
receives each color's HSL values and its 1-based index (1-12):
import {
adjustPalette,
adjustPalettes,
defaultChildrenThemes,
createV5Theme,
} from '@tamagui/config/v5'
// adjust a single palette - desaturate and lighten
const mutedBlue = adjustPalette(blue, (hsl, i) => ({
...hsl,
s: hsl.s * 0.7,
l: Math.min(100, hsl.l * 1.1),
}))
// adjust multiple palettes at once with adjustPalettes
const mutedThemes = adjustPalettes(defaultChildrenThemes, {
// 'default' applies to all colors not explicitly listed
default: {
light: (hsl, i) => ({ ...hsl, s: hsl.s * 0.8 }),
dark: (hsl, i) => ({ ...hsl, s: hsl.s * 0.6, l: hsl.l * 0.9 }),
},
// override specific colors
yellow: {
light: (hsl) => ({ ...hsl, s: hsl.s * 0.5 }),
dark: (hsl, i) => ({ ...hsl, s: hsl.s * (i <= 4 ? 0.3 : 0.8) }),
},
// skip adjustment for specific colors
gray: undefined,
})
const themes = createV5Theme({ childrenThemes: mutedThemes })
The callback receives:
hsl - object with h (hue 0-360), s (saturation 0-100), l (lightness
0-100)i - 1-based index in the palette (1-12)Use the index to apply different adjustments to background colors (1-4), mid-tones (5-8), and foreground colors (9-12).
V5 themes include a rich set of color values accessible via $theme-* tokens.
Every theme includes 12 base colors that shift based on the theme:
<View bg="$color1" /> // Lightest in light mode, darkest in dark mode
<View bg="$color6" /> // Mid-range
<View bg="$color12" /> // Darkest in light mode, lightest in dark mode
Standard semantic values that adapt to each theme:
background, backgroundHover, backgroundPress, backgroundFocuscolor, colorHover, colorPress, colorFocusborderColor, borderColorHover, borderColorPress, borderColorFocusplaceholderColor, outlineColorForeground and background colors with opacity variants. The naming uses the
opacity value without the decimal point (e.g., 01 = 10%, 0025 = 2.5%).
color01 (10%), color0075 (7.5%), color005 (5%), color0025 (2.5%),
color002 (2%), color001 (1%)background01 (10%), background0075 (7.5%), background005 (5%),
background0025 (2.5%), background002 (2%), background001 (1%)background02 (20%), background04 (40%), background06 (60%),
background08 (80%)Always available regardless of theme, with opacity variants:
white, white0 (transparent), white02, white04, white06, white08black, black0 (transparent), black02, black04, black06, black08white1-white12, black1-black12 (12-step scales)Pre-tuned shadow opacities (black) for light and dark modes:
shadow1 through shadow8 (light shadows are subtler, dark are stronger)shadowColor - Default shadow color for the themePre-tuned highlight opacities (white) for light and dark modes:
highlight1 through highlight8 (matching shadow scale, useful for glows/overlays)All color themes expose their full 12-step scale:
<View bg="$blue5" />
<Text color="$red11" />
<View borderColor="$green7" />
Available scales: gray1-12, blue1-12, red1-12, green1-12, yellow1-12,
orange1-12, pink1-12, purple1-12, teal1-12
The neutral scale maintains sufficient contrast on both light and dark
backgrounds - useful for text or UI that needs to work regardless of theme:
neutral1-neutral12Every theme includes an inverted accent scale for contrast elements:
accent1-accent12 - Inverted palette (light colors in dark mode, vice
versa)accentBackground, accentColor - Quick access to accent bg/fg<Button theme="accent">Primary Action</Button>
<View bg="$accentBackground" color="$accentColor" />
V5 includes these color themes by default:
Each color theme has 12 steps following the Radix Colors pattern.
Use any color theme with the theme prop:
<Button theme="blue">Blue button</Button>
<Card theme="purple">Purple card</Card>
Access individual color values in any theme:
<View bg="$orange5" borderColor="$teal7" />
<Text color="$pink11" />
V5 includes default templates that control how theme values map to palette
indices. Available templates: base, surface1, surface2, surface3,
alt1, alt2, inverse.
For more details on how templates work, see the Creating Custom Themes Guide.
V5 keeps component themes because they solve a hard problem — how do you change
the theme of an entire area (like from dark to dark_green) but have the
components inside it still keep their relative shades?
One of the great things in Tamagui is being able to set component themes, and sub-themes, and have an entire area of your UI re-theme and look perfect.
We'd exported just having the ability to set a default theme, like "surface1 =>
4", but then if you try and re-theme a component to green, that would
conflict. For example, if you made your default <Button theme="surface3" />,
then if someone uses it <Button theme="green" /> now it's green, but not the
Button green that is typically stronger than your base green.
So for now we've left them on! We do want to move away from component themes in
general as they can be tricky to understand. But the benefits are also strong,
and until we find a great solution we're leaving them. You can always set
componentThemes: false in your createV5Themes function to turn it off.
V5 config includes these default settings:
<PropsTable data={[ { name: 'defaultFont', description: '"body"' }, { name: 'fastSchemeChange', description: 'true - Uses DynamicColorIOS on iOS for fast light/dark theme changes.', }, { name: 'shouldAddPrefersColorThemes', description: 'true - Generates CSS for prefers-color-scheme.', }, { name: 'allowedStyleValues', description: '"somewhat-strict-web" - Allows web-only values like vh, vw.', }, { name: 'addThemeClassName', description: '"html" - Adds theme className to html tag for CSS variable access.', }, { name: 'onlyAllowShorthands', description: 'true - Removes types for longhand versions of shorthands.', }, { name: 'styleCompat', description: '"react-native" - Aligns flex defaults to React Native (flexBasis: 0).', }, ]} />
V5 includes an expanded set of media queries with height-based, max-width, and min-width variants.
The $sm through $xxl breakpoints match Tailwind CSS exactly (640, 768, 1024,
1280, 1536), making it easy to work across both ecosystems. The smaller
breakpoints ($xxxs, $xxs, $xs) provide finer control below 640px, which is
especially useful for container queries where you need granularity at smaller
sizes.
<PropsTable data={[ { name: 'xxxs', description: 'minWidth: 260' }, { name: 'xxs', description: 'minWidth: 340' }, { name: 'xs', description: 'minWidth: 460' }, { name: 'sm', description: 'minWidth: 640' }, { name: 'md', description: 'minWidth: 768' }, { name: 'lg', description: 'minWidth: 1024' }, { name: 'xl', description: 'minWidth: 1280' }, { name: 'xxl', description: 'minWidth: 1536' }, ]} />
<PropsTable data={[ { name: 'max-xxxs', description: 'maxWidth: 260' }, { name: 'max-xxs', description: 'maxWidth: 340' }, { name: 'max-xs', description: 'maxWidth: 460' }, { name: 'max-sm', description: 'maxWidth: 640' }, { name: 'max-md', description: 'maxWidth: 768' }, { name: 'max-lg', description: 'maxWidth: 1024' }, { name: 'max-xl', description: 'maxWidth: 1280' }, { name: 'max-xxl', description: 'maxWidth: 1536' }, ]} />
<PropsTable data={[ { name: 'height-sm', description: 'minHeight: 640' }, { name: 'height-md', description: 'minHeight: 768' }, { name: 'height-lg', description: 'minHeight: 1024' }, ]} />
<PropsTable data={[ { name: 'max-height-sm', description: 'maxHeight: 640' }, { name: 'max-height-md', description: 'maxHeight: 768' }, { name: 'max-height-lg', description: 'maxHeight: 1024' }, ]} />
<Notice> Height queries have higher CSS specificity than width queries. When both match, height wins. </Notice><PropsTable data={[ { name: 'touchable', description: 'pointer: coarse (touch devices)' }, { name: 'hoverable', description: 'hover: hover (devices with hover capability)' }, ]} />
These names describe intent rather than CSS implementation details — you're thinking "is this a touch device?" not "what's the pointer media query name?" They also read naturally in code:
<Button
$touchable={{ p: '$4', minH: 44 }}
$hoverable={{ hoverStyle: { bg: '$color5' } }}
/>
On web, these aren't exact opposites. A laptop with a touchscreen might have
pointer: fine (trackpad as primary input) but still support touch. Having both
lets you target exactly what you mean.
V5 includes Tailwind-aligned shorthands. With onlyAllowShorthands: true (the
default), only the short forms are available in types.
<PropsTable data={[{ name: 'text', description: 'textAlign' }]} />
<PropsTable data={[ { name: 'p', description: 'padding' }, { name: 'pt', description: 'paddingTop' }, { name: 'pb', description: 'paddingBottom' }, { name: 'pl', description: 'paddingLeft' }, { name: 'pr', description: 'paddingRight' }, { name: 'px', description: 'paddingHorizontal' }, { name: 'py', description: 'paddingVertical' }, { name: 'm', description: 'margin' }, { name: 'mt', description: 'marginTop' }, { name: 'mb', description: 'marginBottom' }, { name: 'ml', description: 'marginLeft' }, { name: 'mr', description: 'marginRight' }, { name: 'mx', description: 'marginHorizontal' }, { name: 'my', description: 'marginVertical' }, ]} />
<PropsTable data={[ { name: 't', description: 'top' }, { name: 'b', description: 'bottom' }, { name: 'l', description: 'left' }, { name: 'r', description: 'right' }, { name: 'minW', description: 'minWidth' }, { name: 'maxW', description: 'maxWidth' }, { name: 'minH', description: 'minHeight' }, { name: 'maxH', description: 'maxHeight' }, { name: 'z', description: 'zIndex' }, ]} />
<PropsTable data={[ { name: 'grow', description: 'flexGrow' }, { name: 'shrink', description: 'flexShrink' }, { name: 'items', description: 'alignItems' }, { name: 'self', description: 'alignSelf' }, { name: 'content', description: 'alignContent' }, { name: 'justify', description: 'justifyContent' }, ]} />
<PropsTable data={[ { name: 'bg', description: 'backgroundColor' }, { name: 'rounded', description: 'borderRadius' }, { name: 'select', description: 'userSelect' }, ]} />
Production Optimization - Theme JS can grow to 20KB or more. Since Tamagui can hydrate themes from CSS variables, you can remove the themes JS from your client bundle for better Lighthouse scores.
This works because Tamagui generates CSS variables for all theme values. On the client, it reads these from the DOM instead of needing the JS object.
import { defaultConfig, themes } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
// only load themes on server - client hydrates from CSS
// for non-One Vite apps, use import.meta.env.SSR instead
themes: process.env.VITE_ENVIRONMENT === 'client' ? ({} as typeof themes) : themes,
})
import { animations } from '@tamagui/config/v5-css'
2xl → xxl, 2xs → xxs, max2Xl →
max-xxlflexBasis: 0 by default (React Native
style)defaultPosition (defaults to static)