| name | tamagui-core |
| description | Guide for building cross-platform React Native and Expo applications with Tamagui. Use this skill when implementing UI components, styling, theming, responsive design, or animations with Tamagui. Applies to tasks involving styled(), tokens, themes, media queries, variants, or TamaguiProvider setup. |
Tamagui Core
Tamagui is a universal UI library for React Native and Expo that provides 100% parity between web and native with an optimizing compiler. This skill covers core styling, theming, and component patterns.
Quick Start with Expo Go
Installation
bunx create-expo-app@latest my-app -t expo-template-blank-typescript
cd my-app
bun add tamagui @tamagui/config
Configuration
Create tamagui.config.ts at the project root:
import { config } from '@tamagui/config/v4'
import { createTamagui } from 'tamagui'
export const tamaguiConfig = createTamagui(config)
export default tamaguiConfig
export type Conf = typeof tamaguiConfig
declare module 'tamagui' {
interface TamaguiCustomConfig extends Conf {}
}
Provider Setup
Wrap the app root with TamaguiProvider:
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from './tamagui.config'
export default function App() {
return (
<TamaguiProvider config={tamaguiConfig} defaultTheme="light">
{/* App content */}
</TamaguiProvider>
)
}
Babel Configuration (Optional but Recommended)
Update babel.config.js for compiler optimization:
module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
plugins: [
[
'@tamagui/babel-plugin',
{
components: ['tamagui'],
config: './tamagui.config.ts',
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
},
],
],
}
}
styled() Function
The styled() function creates typed, optimized components:
import { View, Text, styled } from 'tamagui'
export const Card = styled(View, {
backgroundColor: '$background',
borderRadius: '$4',
padding: '$4',
variants: {
elevated: {
true: {
shadowColor: '$shadowColor',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
},
},
size: {
small: { padding: '$2' },
medium: { padding: '$4' },
large: { padding: '$6' },
},
} as const,
defaultVariants: {
size: 'medium',
},
})
Variant Patterns
Boolean variants use true/false keys:
variants: {
centered: {
true: {
alignItems: 'center',
justifyContent: 'center',
},
},
}
Token-spread variants use ... prefix to map token scales:
variants: {
size: {
'...size': (size, { tokens }) => ({
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
}),
},
}
Functional variants receive the value and extras:
variants: {
width: (val) => ({ width: val }),
}
Property Order
Style property order matters for overrides. Properties defined later take precedence:
const Button = styled(View, {
backgroundColor: '$blue',
...props,
borderRadius: '$4',
})
Tokens
Tokens are design system variables defined via createTokens():
import { createTokens } from 'tamagui'
const tokens = createTokens({
size: {
0: 0,
1: 4,
2: 8,
3: 12,
4: 16,
},
space: {
0: 0,
1: 4,
2: 8,
'-1': -4,
'-2': -8,
},
radius: {
0: 0,
1: 4,
2: 8,
full: 9999,
},
color: {
white: '#fff',
black: '#000',
primary: '#007AFF',
},
zIndex: {
0: 0,
1: 100,
2: 200,
},
})
Token Usage
Access tokens with $ prefix in style values:
<View padding="$4" backgroundColor="$primary" borderRadius="$2" />
Token categories auto-map to CSS properties:
size → width, height, minWidth, maxWidth, minHeight, maxHeight
space → padding, margin, gap
radius → borderRadius
color → color, backgroundColor, borderColor
zIndex → zIndex
Custom Token Categories
Define arbitrary token groups:
const tokens = createTokens({
icon: {
small: 16,
medium: 24,
large: 32,
},
})
Themes
Themes provide contextual styling that changes based on the React tree:
import { createTamagui } from 'tamagui'
const config = createTamagui({
tokens,
themes: {
light: {
background: '#fff',
color: '#000',
primary: '$color.primary',
},
dark: {
background: '#000',
color: '#fff',
primary: '$color.primary',
},
light_accent: {
background: '$color.primary',
color: '#fff',
},
dark_accent: {
background: '$color.primary',
color: '#000',
},
},
})
Theme Component
Change themes anywhere in the component tree:
import { Theme, YStack, Text } from 'tamagui'
function App() {
return (
<Theme name="dark">
<YStack backgroundColor="$background">
<Text color="$color">Dark theme text</Text>
<Theme name="accent">
{/* Resolves to "dark_accent" */}
<Text color="$color">Accent theme text</Text>
</Theme>
<Theme inverse>
{/* Switches to "light" */}
<Text>Inverted theme text</Text>
</Theme>
</YStack>
</Theme>
)
}
useTheme Hook
Access theme values programmatically:
import { useTheme } from 'tamagui'
function Component() {
const theme = useTheme()
const bgColor = theme.background.val
const optimizedBg = theme.background.get()
return <View style={{ backgroundColor: bgColor }} />
}
Media Queries & Responsive Design
Configuration
Define breakpoints in config:
const config = createTamagui({
media: {
xs: { maxWidth: 660 },
sm: { maxWidth: 860 },
md: { maxWidth: 980 },
lg: { maxWidth: 1120 },
xl: { minWidth: 1121 },
short: { maxHeight: 820 },
tall: { minHeight: 821 },
hoverNone: { hover: 'none' },
pointerCoarse: { pointer: 'coarse' },
},
})
Inline Media Props
Apply responsive styles with $ prefix:
<YStack
padding="$4"
$sm={{ padding: '$2' }}
$md={{ padding: '$3' }}
flexDirection="column"
$lg={{ flexDirection: 'row' }}
/>
useMedia Hook
Access breakpoints programmatically:
import { useMedia } from 'tamagui'
function Component() {
const media = useMedia()
return (
<YStack
backgroundColor={media.sm ? '$blue' : '$green'}
{...(media.lg && { padding: '$6' })}
>
{media.sm ? <MobileView /> : <DesktopView />}
</YStack>
)
}
Performance note: useMedia uses proxies to track accessed keys and only re-renders when those specific breakpoints change.
Animations
Animation Drivers
Tamagui supports three swappable animation drivers:
- CSS Animations (web-optimized):
@tamagui/animations-css
- React Native Animated:
@tamagui/animations-react-native
- Reanimated:
@tamagui/animations-moti
Configuration
import { createAnimations } from '@tamagui/animations-css'
const animations = createAnimations({
fast: 'ease-in 150ms',
medium: 'ease-in 300ms',
slow: 'ease-in 450ms',
bouncy: 'cubic-bezier(0.175, 0.885, 0.32, 1.275) 300ms',
})
const config = createTamagui({
animations,
})
For spring animations with React Native Animated:
import { createAnimations } from '@tamagui/animations-react-native'
const animations = createAnimations({
fast: {
type: 'spring',
damping: 20,
mass: 1.2,
stiffness: 250,
},
slow: {
type: 'spring',
damping: 15,
stiffness: 100,
},
})
Using Animations
<YStack
animation="fast"
enterStyle={{ opacity: 0, scale: 0.9 }}
opacity={1}
scale={1}
hoverStyle={{ scale: 1.05 }}
pressStyle={{ scale: 0.95 }}
/>
AnimatePresence for Exit Animations
import { AnimatePresence } from 'tamagui'
function Modal({ isOpen, children }) {
return (
<AnimatePresence>
{isOpen && (
<YStack
key="modal"
animation="medium"
enterStyle={{ opacity: 0, y: 20 }}
exitStyle={{ opacity: 0, y: -20 }}
opacity={1}
y={0}
>
{children}
</YStack>
)}
</AnimatePresence>
)
}
Important: Once animation prop is added, keep it present. Use null, false, or undefined to disable rather than removing the prop conditionally.
Pseudo-State Styles
Apply interactive state styles:
<Button
backgroundColor="$blue"
hoverStyle={{ backgroundColor: '$blueHover' }}
pressStyle={{ backgroundColor: '$bluePress', scale: 0.98 }}
focusStyle={{ outlineWidth: 2, outlineColor: '$blueFocus' }}
focusVisibleStyle={{ outlineWidth: 2 }}
disabledStyle={{ opacity: 0.5 }}
/>
Platform-Specific Styles
Target specific platforms:
<YStack
padding="$4"
$platform-native={{ padding: '$2' }}
$platform-web={{ cursor: 'pointer' }}
$platform-ios={{ paddingTop: '$6' }}
$platform-android={{ elevation: 4 }}
/>
Groups (Parent-Child Styling)
Enable parent-based child styling:
<YStack group="card" backgroundColor="$background">
<Text
color="$color"
$group-card-hover={{ color: '$primary' }}
$group-card-press={{ opacity: 0.8 }}
>
Hover the card to change my color
</Text>
</YStack>
Component Composition with Context
Create compound components with shared context:
import { createStyledContext, styled, withStaticProperties } from 'tamagui'
const ButtonContext = createStyledContext({
size: '$4' as SizeTokens,
})
const ButtonFrame = styled(View, {
name: 'Button',
context: ButtonContext,
backgroundColor: '$primary',
borderRadius: '$4',
variants: {
size: {
'...size': (size, { tokens }) => ({
padding: tokens.space[size],
}),
},
},
})
const ButtonText = styled(Text, {
name: 'ButtonText',
context: ButtonContext,
color: '$color',
variants: {
size: {
'...size': (size, { tokens }) => ({
fontSize: tokens.size[size],
}),
},
},
})
export const Button = withStaticProperties(ButtonFrame, {
Props: ButtonContext.Provider,
Text: ButtonText,
})
<Button size="$4">
<Button.Text>Click me</Button.Text>
</Button>
The asChild Pattern
The asChild prop passes styles and behavior to a child component:
import { Button } from 'tamagui'
import { Link } from 'expo-router'
<Button asChild>
<Link href="/settings">Settings</Link>
</Button>
Common Uses:
- Wrapping navigation links with styled buttons
- Applying trigger styles to custom elements
- Dialog/Popover/Tooltip triggers
<Dialog.Trigger asChild>
<Button theme="red">Delete</Button>
</Dialog.Trigger>
<Popover.Trigger asChild>
<Button icon={<MoreHorizontal />} circular />
</Popover.Trigger>
Requirements for asChild targets:
- Must accept and forward style props
- Must accept
onPress/onClick handlers
- Must be a valid React element
Size Scaling System
Tamagui's size tokens scale multiple properties from a single size prop:
const ScaledComponent = styled(View, {
name: 'ScaledComponent',
variants: {
size: {
'...size': (size, { tokens }) => ({
height: tokens.size[size] ?? size,
paddingHorizontal: tokens.space[size],
borderRadius: tokens.radius[size] ?? tokens.radius.$4,
}),
},
} as const,
})
Size Token Convention:
| Token | Use Case |
|---|
$2 | Compact/dense UI |
$3 | Small elements |
$4 | Default (recommended as defaultVariant) |
$5 | Large elements |
$6 | Extra large/hero |
Token Spread Patterns:
'...size': (size, { tokens }) => ({ ... })
'...space': (space, { tokens }) => ({ ... })
'...radius': (radius, { tokens }) => ({ ... })
ThemeableStack
For components that need theme-aware backgrounds with hover/press states:
import { ThemeableStack, styled } from 'tamagui'
const InteractiveCard = styled(ThemeableStack, {
name: 'InteractiveCard',
padding: '$4',
borderRadius: '$4',
})
Used by: Card, Checkbox, Group, ListItem, and other interactive containers.
Cross-Platform Patterns
Adapt Component
Convert between components based on platform:
import { Adapt, Dialog, Sheet } from 'tamagui'
function ResponsiveDialog({ children }) {
return (
<Dialog>
{/* On mobile, convert Dialog.Content to Sheet */}
<Adapt when="sm" platform="touch">
<Sheet modal dismissOnSnapToBottom>
<Sheet.Frame padding="$4">
<Adapt.Contents /> {/* Renders Dialog content here */}
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>{children}</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
}
Platform-Specific Styles
<YStack
padding="$4"
$platform-web={{ cursor: 'pointer' }}
$platform-native={{ padding: '$2' }}
$platform-ios={{ paddingTop: '$6' }}
$platform-android={{ elevation: 4 }}
/>
Web-Only Features
Some features only work on web:
FocusScope - Focus trapping
cursor property
- CSS animations with
@tamagui/animations-css
outlineStyle, outlineOffset
Native Adaptations
- Use
Sheet instead of Dialog on mobile for better UX
elevation for Android shadows
- Gesture handlers for swipe interactions
Accessibility Quick Reference
Required Elements
| Component | Requirements |
|---|
| Dialog | Must have Dialog.Title and Dialog.Description |
| Button | Text content or aria-label for icon-only |
| Input | Associated Label with matching htmlFor/id |
| Image | alt prop or aria-label |
Focus Management
import { FocusScope } from 'tamagui'
<Dialog.Content>
<FocusScope trapped restoreFocus>
{/* Focus trapped inside, restored on close */}
</FocusScope>
</Dialog.Content>
Common Patterns
<Button icon={<MenuIcon />} aria-label="Open menu" />
<Label htmlFor="email">Email</Label>
<Input
id="email"
aria-describedby={error ? 'email-error' : undefined}
aria-invalid={!!error}
/>
{error && <Text id="email-error">{error}</Text>}
<Button aria-busy={isLoading} disabled={isLoading}>
{isLoading ? <Spinner /> : 'Submit'}
</Button>
forceMount for Animations
Keep elements mounted for exit animations:
<Dialog>
<Dialog.Portal forceMount>
<AnimatePresence>
{open && (
<Dialog.Content
key="content"
animation="medium"
enterStyle={{ opacity: 0, y: 10 }}
exitStyle={{ opacity: 0, y: 10 }}
>
{children}
</Dialog.Content>
)}
</AnimatePresence>
</Dialog.Portal>
</Dialog>
Modular Design
For comprehensive guidance on component architecture, file organization, and building component libraries, see the modular-design skill. It covers:
- Atomic design structure (atoms, molecules, organisms)
- Compound component patterns
- File naming and export conventions
- Component creation checklists
Best Practices
- Use tokens consistently - Define all design values as tokens for maintainability
- Prefer inline styles for variants - The compiler optimizes these to static CSS
- Use useMedia sparingly - Prefer inline
$sm={{ }} syntax when possible
- Keep themes focused on colors - Other values should be tokens
- Use enterStyle for mount animations - SSR-safe and compiler-optimized
- Leverage the compiler - Add
// debug comment to see optimization output
- Use groups for hover effects - More performant than manual state management
- Clear cache after config changes - Run
bunx expo start -c
- Use full property names - Avoid shorthands like
ai, jc, br, p, px, py as they may not be typed correctly. Use alignItems, justifyContent, borderRadius, padding, paddingHorizontal, paddingVertical instead
- Use textProps for Button text styling -
textTransform and other text styles don't apply to Button's internal text via styled(). Use textProps={{ style: {...} }}
- Use useTheme() for dynamic theme values - When passing theme tokens to non-Tamagui components (like icons), access the raw value via
theme.tokenName?.val
Common Patterns
Responsive Stack Direction
<XStack
flexDirection="column"
$md={{ flexDirection: 'row' }}
gap="$4"
>
<Item />
<Item />
</XStack>
Theme-Aware Component
const ThemedCard = styled(YStack, {
backgroundColor: '$background',
borderColor: '$borderColor',
borderWidth: 1,
$theme-dark: {
borderColor: '$gray800',
},
$theme-light: {
borderColor: '$gray200',
},
})
Accessible Button
const AccessibleButton = styled(View, {
tag: 'button',
role: 'button',
backgroundColor: '$primary',
padding: '$3',
borderRadius: '$2',
cursor: 'pointer',
hoverStyle: { backgroundColor: '$primaryHover' },
pressStyle: { backgroundColor: '$primaryPress' },
focusVisibleStyle: {
outlineWidth: 2,
outlineColor: '$primaryFocus',
outlineStyle: 'solid',
},
})
References
For detailed API documentation, see the references folder:
references/components.md - UI component catalog
references/config-options.md - Full configuration reference
references/troubleshooting.md - Common issues and solutions