| name | tamagui |
| description | Universal React UI framework for web and native. Use when building cross-platform apps with Tamagui,
creating styled components with `styled()`, configuring design tokens/themes, using Tamagui UI components,
or working with animations. Triggers: "tamagui", "styled()", "$token", "XStack/YStack", "useTheme",
"@tamagui/*" imports, "createStyledContext", "variants".
|
| version | 1.0.0 |
Tamagui Skill
Universal React UI framework for web and native with an optimizing compiler.
Getting Project-Specific Config
Before writing Tamagui code, get the project's actual configuration:
npx tamagui generate-prompt
This outputs tamagui-prompt.md with the project's specific:
- Design tokens (space, size, radius, color, zIndex)
- Theme names and hierarchy
- Available components
- Media query breakpoints
- Shorthand properties
- Font families
Always reference this file for token/theme/media query names rather than guessing or using defaults.
Core Concepts
styled() Function
Create components by extending existing ones:
import { View, Text, styled } from '@tamagui/core'
const Card = styled(View, {
padding: '$4',
backgroundColor: '$background',
borderRadius: '$4',
variants: {
size: {
small: { padding: '$2' },
large: { padding: '$6' },
},
elevated: {
true: {
boxShadow: '0 8px 24px $shadow4',
},
},
} as const,
defaultVariants: {
size: 'small',
},
})
<Card size="large" elevated />
Key rules:
- Always use
as const on variants objects
- Tokens use
$ prefix: $4, $background, $color11
- Prop order matters - later props override earlier ones
- Variants defined later in the object override earlier ones
Stack Components
import { XStack, YStack, ZStack } from 'tamagui'
<YStack gap="$4" padding="$4">
<XStack justifyContent="space-between" alignItems="center">
<Text>Label</Text>
<Button>Action</Button>
</XStack>
</YStack>
Themes
Themes nest and combine hierarchically:
import { Theme } from 'tamagui'
<Theme name="dark">
{}
<Theme name="blue">
{}
<Button>Blue button on dark</Button>
</Theme>
</Theme>
const theme = useTheme()
console.log(theme.background.val)
console.log(theme.color11.val)
12-step color scale convention:
$color1-4: backgrounds (subtle to emphasized)
$color5-6: borders, separators
$color7-8: hover/active states
$color9-10: solid backgrounds
$color11-12: text (low to high contrast)
Responsive Styles
Use media query props (check your tamagui-prompt.md for actual breakpoint names):
<YStack
padding="$4"
$gtSm={{ padding: '$6' }}
$gtMd={{ padding: '$8' }}
flexDirection="column"
$gtLg={{ flexDirection: 'row' }}
/>
const media = useMedia()
if (media.gtMd) {
}
Animations
import { AnimatePresence } from 'tamagui'
<AnimatePresence>
{show && (
<YStack
key="modal" // key required for exit animations
transition="quick"
enterStyle={{ opacity: 0, y: -20 }}
exitStyle={{ opacity: 0, y: 20 }}
opacity={1}
y={0}
/>
)}
</AnimatePresence>
Animation drivers:
@tamagui/animations-css - web only, CSS transitions
@tamagui/animations-react-native - native Animated API
@tamagui/animations-reanimated - best native performance
@tamagui/animations-motion - spring physics
CSS driver uses easing strings, others support spring physics.
Compound Components
Use createStyledContext for components that share state:
import { createStyledContext, styled, View, Text } from '@tamagui/core'
import { withStaticProperties } from '@tamagui/helpers'
const CardContext = createStyledContext({ size: 'medium' as 'small' | 'medium' | 'large' })
const CardFrame = styled(View, {
context: CardContext,
padding: '$4',
backgroundColor: '$background',
variants: {
size: {
small: { padding: '$2' },
medium: { padding: '$4' },
large: { padding: '$6' },
},
} as const,
})
const CardTitle = styled(Text, {
context: CardContext,
fontWeight: 'bold',
variants: {
size: {
small: { fontSize: '$4' },
medium: { fontSize: '$5' },
large: { fontSize: '$6' },
},
} as const,
})
export const Card = withStaticProperties(CardFrame, {
Title: CardTitle,
})
<Card size="large">
<Card.Title>Large Title</Card.Title>
</Card>
Common Patterns
Dialog with Adapt (Sheet on Mobile)
import { Dialog, Sheet, Adapt, Button } from 'tamagui'
<Dialog>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Adapt when="sm" platform="touch">
<Sheet modal dismissOnSnapToBottom>
<Sheet.Frame padding="$4">
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Dialog.Portal>
<Dialog.Overlay
key="overlay"
transition="quick"
opacity={0.5}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
<Dialog.Content
key="content"
transition="quick"
enterStyle={{ opacity: 0, scale: 0.95 }}
exitStyle={{ opacity: 0, scale: 0.95 }}
>
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
<Dialog.Close asChild>
<Button>Close</Button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
Form with Input/Label
import { Input, Label, YStack, XStack, Button } from 'tamagui'
<YStack gap="$4" padding="$4">
<YStack gap="$2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
placeholder="email@example.com"
autoCapitalize="none"
keyboardType="email-address"
/>
</YStack>
<XStack gap="$2" justifyContent="flex-end">
<Button variant="outlined">Cancel</Button>
<Button theme="blue">Submit</Button>
</XStack>
</YStack>
Anti-Patterns
❌ The animation prop
There is no animation prop. It is the most commonly invented one. The prop is
transition, and its value is a TransitionProp: a configured animation name,
an object, or an array. A CSS transition string is not one.
<View animation="quick" />
<View transition="all 0.2s ease" />
<View transition="quick" />
Use animatedBy="<driver>" only when the config registers more than one driver.
❌ Assuming modern style props are web-only
backdropFilter, mixBlendMode, boxShadow, filter, backgroundImage,
transition, cursor, and userSelect are first-class typed props that React
Native's New Architecture implements natively. backdropFilter is a real native
gaussian backdrop blur, so frosting a surface needs no separate blur view
package. Treating one of these as a no-op on iOS is a stale assumption.
<View shadowColor="$shadowColor" shadowOffset={{ width: 0, height: 8 }} shadowRadius={10} />
<View boxShadow="0 8px 24px $shadow4" />
Tamagui is moving this way itself: a config setting removes the border, outline,
and shadow longhands from the type system in favor of the combined border,
outline, and boxShadow props, because mixing shorthand and longhand fights
over atomic CSS specificity.
❌ Hardcoded values instead of tokens
<View padding={16} backgroundColor="#fff" />
<View padding="$4" backgroundColor="$background" />
❌ Missing as const on variants
variants: {
size: { small: {...}, large: {...} }
}
variants: {
size: { small: {...}, large: {...} }
} as const
❌ Platform detection in styled()
const Box = styled(View, {
padding: Platform.OS === 'web' ? 10 : 20,
})
const Box = styled(View, {
padding: 20,
'$platform-web': { padding: 10 },
})
❌ exitStyle without AnimatePresence
{show && <View exitStyle={{ opacity: 0 }} />}
<AnimatePresence>
{show && <View key="box" exitStyle={{ opacity: 0 }} />}
</AnimatePresence>
❌ Dynamic values that prevent extraction
const dynamicPadding = isPremium ? '$6' : '$4'
<View padding={dynamicPadding} />
<View padding={isPremium ? '$6' : '$4'} />
❌ Wrong media query order
<View $gtMd={{ padding: '$8' }} padding="$4" />
<View padding="$4" $gtMd={{ padding: '$8' }} />
❌ Spring animations with CSS driver
import { createAnimations } from '@tamagui/animations-css'
const anims = createAnimations({
bouncy: { type: 'spring', damping: 10 }
})
const anims = createAnimations({
bouncy: 'cubic-bezier(0.68, -0.55, 0.265, 1.55) 300ms'
})
Compiler Optimization
The Tamagui compiler extracts static styles to CSS at build time. For styles to be extracted:
- Use tokens -
$4 extracts, 16 may not
- Inline ternaries -
padding={x ? '$4' : '$2'} extracts
- Avoid runtime variables - computed values don't extract
- Use variants - better than conditional props
Check if extraction is working:
- Look for
data-tamagui attributes in dev mode
- Bundle size should be smaller with compiler enabled
- Styles should appear as CSS classes, not inline
TypeScript
import { GetProps, styled, View } from '@tamagui/core'
const MyComponent = styled(View, {
variants: {
size: { small: {}, large: {} }
} as const,
})
type MyComponentProps = GetProps<typeof MyComponent>
interface ExtendedProps extends MyComponentProps {
onCustomEvent?: () => void
}