重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
npx skills add https://github.com/ramarivera/tamagui-skills --skill tamagui-v2适用于 Web 和 Native 的通用 React UI 框架 (v1.144.0+)
此技能为使用 Tamagui 的样式系统、组件库、Bento 高级组件和 Takeout 入门套件构建跨平台 React 应用程序提供全面的指导。
Tamagui 是一个通用的 UI 框架,允许您编写一次代码,并以最佳性能部署到 Web 和 React Native。其特点包括:
styled() 函数版本: 1.144.0+
平台: Web (React)、iOS/Android (React Native)、Expo
许可证: 开源 (MIT) + Bento/Takeout(商业许可证)
当遇到以下触发词时,请激活此技能:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
通过扩展现有组件创建自定义组件:
import { View, Text, styled } from '@tamagui/core'
// 简单的样式化组件
export const Card = styled(View, {
padding: '$4',
backgroundColor: '$background',
borderRadius: '$4',
borderWidth: 1,
borderColor: '$borderColor',
})
// 带变体
export const Button = styled(View, {
padding: '$3',
borderRadius: '$2',
backgroundColor: '$blue10',
cursor: 'pointer',
variants: {
variant: {
primary: {
backgroundColor: '$blue10',
},
secondary: {
backgroundColor: '$gray5',
},
outlined: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '$borderColor',
},
},
size: {
small: { padding: '$2' },
medium: { padding: '$3' },
large: { padding: '$4' },
},
} as const,
defaultVariants: {
variant: 'primary',
size: 'medium',
},
})
// 用法
<Button variant="outlined" size="large">Click Me</Button>
Stacks 是 Tamagui 布局的基础:
import { XStack, YStack, ZStack, Text, Button } from 'tamagui'
export function LayoutExample() {
return (
<YStack gap="$4" padding="$4">
{/* 水平堆栈 */}
<XStack gap="$2" justifyContent="space-between" alignItems="center">
<Text>Label</Text>
<Button>Action</Button>
</XStack>
{/* 带响应式间距的垂直堆栈 */}
<YStack
gap="$4"
$gtSm={{ gap: '$6' }} // 在小屏幕及以上使用更大的间距
>
<Card>Item 1</Card>
<Card>Item 2</Card>
</YStack>
{/* 覆盖堆栈 (position: relative) */}
<ZStack width={300} height={200}>
<View backgroundColor="$blue5" fullscreen />
<Text position="absolute" top="$4" left="$4">
Overlay Text
</Text>
</ZStack>
</YStack>
)
}
设计令牌为您的应用程序提供一致的值:
import { View, Text, createTamagui } from '@tamagui/core'
// 令牌在 createTamagui 配置中定义
const config = createTamagui({
tokens: {
color: {
white: '#fff',
black: '#000',
blue: '#0066cc',
},
space: {
1: 4,
2: 8,
3: 12,
4: 16,
},
size: {
sm: 100,
md: 200,
lg: 300,
},
radius: {
1: 4,
2: 8,
3: 12,
4: 16,
},
},
})
// 使用带 $ 前缀的令牌
<View
padding="$4" // 使用 space.4 = 16px
backgroundColor="$blue" // 使用 color.blue
borderRadius="$3" // 使用 radius.3 = 12px
width="$md" // 使用 size.md = 200px
/>
// 令牌在 styled() 中有效
const StyledCard = styled(View, {
padding: '$4',
margin: '$2',
backgroundColor: '$background',
borderRadius: '$4',
})
Tamagui 使用语义化颜色阶梯系统 (1-12) 来实现一致的主题:
import { YStack, Text, Theme } from 'tamagui'
export function ThemeExample() {
return (
<YStack
backgroundColor="$color1" // 浅色背景
borderColor="$color6" // 常规边框
padding="$4"
>
<Text color="$color12">标题 (最高对比度)</Text>
<Text color="$color11">正文文本 (高对比度)</Text>
<Text color="$color10">弱化文本 (低对比度)</Text>
{/* 应用子主题 */}
<Theme name="blue">
<YStack
backgroundColor="$color9" // 蓝色实心背景
padding="$3"
borderRadius="$2"
>
<Text color="$color1">蓝色背景上的白色文字</Text>
</Theme>
</YStack>
</YStack>
)
}
// 动态主题切换
import { useTheme } from '@tamagui/core'
export function ThemedComponent() {
const theme = useTheme()
// 直接访问主题值
console.log(theme.background.val) // 例如:"#ffffff"
console.log(theme.color11.val) // 例如:"#333333"
return <Text color={theme.color11}>主题化文本</Text>
}
使用媒体查询属性实现响应式布局:
import { YStack, Text, useMedia } from 'tamagui'
export function ResponsiveExample() {
const media = useMedia()
return (
<YStack
padding="$4"
$gtSm={{ padding: '$6' }} // > sm 断点
$gtMd={{ padding: '$8' }} // > md 断点
flexDirection="column"
$gtLg={{ flexDirection: 'row' }} // 在大屏幕上使用行布局
>
<Text
fontSize="$4"
$gtSm={{ fontSize: '$5' }}
$gtMd={{ fontSize: '$6' }}
>
响应式文本
</Text>
{/* 基于媒体查询的条件渲染 */}
{media.gtMd && <Text>仅在中屏幕及以上显示</Text>}
</YStack>
)
}
// 在 createTamagui 中配置媒体查询
const config = createTamagui({
media: {
xs: { maxWidth: 660 },
sm: { maxWidth: 800 },
md: { maxWidth: 1020 },
lg: { maxWidth: 1280 },
xl: { maxWidth: 1420 },
xxl: { maxWidth: 1600 },
gtXs: { minWidth: 660 + 1 },
gtSm: { minWidth: 800 + 1 },
gtMd: { minWidth: 1020 + 1 },
gtLg: { minWidth: 1280 + 1 },
},
})
为任何组件添加平滑动画:
import { YStack, Button, AnimatePresence } from 'tamagui'
import { useState } from 'react'
export function AnimationExample() {
const [show, setShow] = useState(false)
return (
<YStack gap="$4">
<Button onPress={() => setShow(!show)}>
切换
</Button>
<AnimatePresence>
{show && (
<YStack
key="animated-box"
animation="quick"
enterStyle={{
opacity: 0,
y: -20,
scale: 0.9,
}}
exitStyle={{
opacity: 0,
y: 20,
scale: 0.9,
}}
opacity={1}
y={0}
scale={1}
backgroundColor="$blue5"
padding="$4"
borderRadius="$4"
>
<Text>动画内容</Text>
</YStack>
)}
</AnimatePresence>
</YStack>
)
}
// 在 createTamagui 中定义动画
import { createAnimations } from '@tamagui/animations-react-native'
const animations = createAnimations({
quick: {
type: 'spring',
damping: 20,
mass: 1.2,
stiffness: 250,
},
bouncy: {
type: 'spring',
damping: 10,
mass: 0.9,
stiffness: 100,
},
})
构建紧密的组件家族:
import { createStyledContext, styled, View, Text } from '@tamagui/core'
import { withStaticProperties } from '@tamagui/helpers'
// 定义上下文
const CardContext = createStyledContext({
size: '$4' as any,
})
// 创建基础组件
const CardFrame = styled(View, {
backgroundColor: '$background',
borderRadius: '$4',
borderWidth: 1,
borderColor: '$borderColor',
context: CardContext,
variants: {
size: {
small: { padding: '$3' },
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,
})
const CardDescription = styled(Text, {
context: CardContext,
color: '$color11',
variants: {
size: {
small: { fontSize: '$3' },
medium: { fontSize: '$4' },
large: { fontSize: '$5' },
},
} as const,
})
// 使用静态属性组合
export const Card = withStaticProperties(CardFrame, {
Title: CardTitle,
Description: CardDescription,
})
// 用法 - size 会级联到所有子组件
<Card size="large">
<Card.Title>大卡片</Card.Title>
<Card.Description>
所有文本自动调整为大型号
</Card.Description>
</Card>
import { Button, Dialog, Input, Label, XStack, YStack } from 'tamagui'
export function LoginForm() {
return (
<YStack gap="$4" padding="$4">
<YStack gap="$2">
<Label htmlFor="email">邮箱</Label>
<Input
id="email"
placeholder="email@example.com"
autoCapitalize="none"
keyboardType="email-address"
/>
</YStack>
<YStack gap="$2">
<Label htmlFor="password">密码</Label>
<Input
id="password"
secureTextEntry
placeholder="••••••••"
/>
</YStack>
<XStack gap="$2" justifyContent="flex-end">
<Button variant="outlined">取消</Button>
<Button theme="blue">登录</Button>
</XStack>
</YStack>
)
}
import { Button, Dialog, XStack, YStack, H2, Paragraph } from 'tamagui'
export function DialogExample() {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>打开对话框</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
key="overlay"
animation="quick"
opacity={0.5}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
<Dialog.Content
bordered
elevate
key="content"
animation={['quick', { opacity: { overshootClamping: true } }]}
enterStyle={{ x: 0, y: -20, opacity: 0, scale: 0.9 }}
exitStyle={{ x: 0, y: 10, opacity: 0, scale: 0.95 }}
gap="$4"
>
<Dialog.Title>确认操作</Dialog.Title>
<Dialog.Description>
您确定要继续吗?
</Dialog.Description>
<XStack alignSelf="flex-end" gap="$2">
<Dialog.Close displayWhenAdapted asChild>
<Button variant="outlined">取消</Button>
</Dialog.Close>
<Dialog.Close asChild>
<Button theme="blue">确认</Button>
</Dialog.Close>
</XStack>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
}
import { Button, Dialog, Sheet, Adapt } from 'tamagui'
export function AdaptiveDialog() {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>打开</Button>
</Dialog.Trigger>
<Adapt when="sm" platform="touch">
<Sheet animation="medium" zIndex={200000} modal dismissOnSnapToBottom>
<Sheet.Frame padding="$4" gap="$4">
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay
animation="lazy"
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
</Sheet>
</Adapt>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
{/* 内容在桌面端显示为对话框,在移动端显示为 Sheet */}
<Dialog.Title>自适应 UI</Dialog.Title>
<Dialog.Description>
在桌面端是对话框,在移动端是 Sheet
</Dialog.Description>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
}
来自 @tamagui/bento 的高级组件(生产使用需要 Bento 许可证)。
可组合的输入组件,集成了 React Hook Form 和 Zod:
import { Input } from '@tamagui/bento/components/inputsParts'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('邮箱格式无效'),
password: z.string().min(8, '密码必须至少 8 个字符'),
})
export function BentoForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
})
return (
<YStack gap="$4">
<Input size="$4">
<Input.Label htmlFor="email">邮箱</Input.Label>
<Input.Box>
<Input.Section>
<Mail size={16} />
</Input.Section>
<Input.Area
id="email"
placeholder="email@example.com"
{...register('email')}
/>
</Input.Box>
{errors.email && (
<Input.Info color="$red10">{errors.email.message}</Input.Info>
)}
</Input>
<Input size="$4">
<Input.Label htmlFor="password">密码</Input.Label>
<Input.Box>
<Input.Area
id="password"
secureTextEntry
placeholder="••••••••"
{...register('password')}
/>
<Input.Section>
<Input.Button>
<Eye size={16} />
</Input.Button>
</Input.Section>
</Input.Box>
{errors.password && (
<Input.Info color="$red10">{errors.password.message}</Input.Info>
)}
</Input>
</YStack>
)
}
主要特性:
htmlFor 绑定的可访问标签FocusContext 的焦点感知容器生产就绪的数据表格,支持排序、过滤和分页:
import { DataTable } from '@tamagui/bento/components/Table'
import { createColumnHelper } from '@tanstack/react-table'
type User = {
id: number
name: string
email: string
role: string
}
const columnHelper = createColumnHelper<User>()
const columns = [
columnHelper.accessor('name', {
header: '姓名',
cell: info => info.getValue(),
}),
columnHelper.accessor('email', {
header: '邮箱',
cell: info => info.getValue(),
}),
columnHelper.accessor('role', {
header: '角色',
cell: info => <Text textTransform="capitalize">{info.getValue()}</Text>,
}),
]
export function UsersTable({ users }: { users: User[] }) {
return (
<DataTable
data={users}
columns={columns}
enableSorting
enableFiltering
enablePagination
pageSize={10}
/>
)
}
使用 FlatList 模式进行高效的列表渲染:
import { FlatList } from '@tamagui/bento/components/List'
import { YStack, Text } from 'tamagui'
export function VirtualizedList({ items }: { items: string[] }) {
return (
<FlatList
data={items}
keyExtractor={(item, index) => `${item}-${index}`}
renderItem={({ item }) => (
<YStack padding="$4" borderBottomWidth={1} borderColor="$borderColor">
<Text>{item}</Text>
</YStack>
)}
estimatedItemSize={60}
/>
)
}
其他模式:
来自 Tamagui Takeout 的全栈入门套件(需要许可证)。
基于文件的路由,支持 SSG/SSR/SPA 模式:
app/
_layout.tsx # 根布局
index.tsx # / (主页)
blog/
[slug].tsx # /blog/my-post (动态路由)
(auth)/ # 路由组 (无 URL 嵌套)
login.tsx # /login
signup.tsx # /signup
+not-found.tsx # 自定义 404 页面
类型安全的导航:
import { Link, useRouter, useParams } from 'one'
export function Navigation() {
const router = useRouter()
return (
<YStack gap="$2">
{/* 声明式导航 */}
<Link href="/">主页</Link>
<Link href="/blog/hello-world">博客文章</Link>
{/* 编程式导航 */}
<Button onPress={() => router.push('/about')}>
前往关于页面
</Button>
</YStack>
)
}
// 动态路由
export function BlogPost() {
const { slug } = useParams<{ slug: string }>()
return <Text>文章:{slug}</Text>
}
服务端数据加载:
import { createLoader } from 'one'
export const loader = createLoader(async (context) => {
const post = await db.query.posts.findFirst({
where: eq(posts.slug, context.params.slug),
})
return { post }
})
export default function BlogPost() {
const { post } = useLoader(loader)
return <Text>{post.title}</Text>
}
具有多种策略的完整身份验证系统:
import { signIn, signOut, useSession } from '~/lib/auth'
export function AuthExample() {
const session = useSession()
if (!session.user) {
return (
<YStack gap="$4">
{/* 邮箱/密码 */}
<Button onPress={() => signIn.email({ email, password })}>
登录
</Button>
{/* OAuth */}
<Button onPress={() => signIn.social({ provider: 'github' })}>
使用 GitHub 登录
</Button>
{/* 魔法链接 */}
<Button onPress={() => signIn.magicLink({ email })}>
发送魔法链接
</Button>
</YStack>
)
}
return (
<YStack gap="$2">
<Text>欢迎,{session.user.name}</Text>
<Button onPress={() => signOut()}>退出登录</Button>
</YStack>
)
}
特性:
使用 PostgreSQL 进行类型安全的数据库查询:
import { db } from '~/db'
import { posts, users } from '~/db/schema'
import { eq, desc } from 'drizzle-orm'
// 定义模式
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
})
// 查询
const allPosts = await db.query.posts.findMany({
orderBy: [desc(posts.createdAt)],
with: {
author: true, // 与 users 表连接
},
})
// 插入
await db.insert(posts).values({
title: '我的文章',
slug: 'my-post',
content: '文章内容...',
authorId: 1,
})
// 更新
await db.update(posts)
.set({ title: '更新后的标题' })
.where(eq(posts.id, 1))
// 删除
await db.delete(posts).where(eq(posts.id, 1))
离线优先的实时同步:
import { useQuery, useMutation } from '@rocicorp/zero/react'
import { z } from '@rocicorp/zero'
// 定义模式
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
createdAt: z.number(),
})
export function TodoList() {
// 实时查询 (自动更新)
const [todos] = useQuery(q => q.todos.orderBy('createdAt', 'desc'))
// 乐观更新
const [addTodo] = useMutation(async (tx, text: string) => {
await tx.todos.insert({
id: crypto.randomUUID(),
text,
completed: false,
createdAt: Date.now(),
})
})
const [toggleTodo] = useMutation(async (tx, id: string) => {
const todo = await tx.todos.get(id)
if (todo) {
await tx.todos.update({
id,
completed: !todo.completed,
})
}
})
return (
<YStack gap="$2">
{todos.map(todo => (
<XStack key={todo.id} gap="$2">
<Checkbox
checked={todo.completed}
onCheckedChange={() => toggleTodo(todo.id)}
/>
<Text>{todo.text}</Text>
</XStack>
))}
</YStack>
)
}
特性:
有关特定主题的详细文档,请参阅以下参考文件:
| 主题 | 参考文件 | 描述 |
|---|---|---|
| 核心样式 | core-styling.md | styled() API、变体系统、createStyledContext、组合模式、TypeScript 集成 |
| 组件 | components.md | Button、Dialog、Sheet、Input、Select、Tabs、Switch、Popover、Stacks、Adapt 模式 |
| 主题 | theming.md | 12 步颜色阶梯系统、主题创建、动态主题切换、useTheme 钩子 |
| 动画 | animations.md | 动画驱动 (CSS、React Native、Reanimated、Motion)、enterStyle/exitStyle、AnimatePresence |
| 配置 | configuration.md | createTamagui 函数、令牌、主题、字体、媒体查询、简写 |
| 编译器 | compiler.md | 静态提取、Babel 优化、原子 CSS 生成、平台特定设置 |
| Bento 表单 | bento-forms.md | React Hook Form 集成、Zod 验证、可组合输入系统、可访问性 |
| Bento 表格 | bento-tables.md | TanStack Table v8 集成、排序、分页、过滤、响应式布局 |
| Bento 列表 | bento-lists.md | FlatList 模式、虚拟化列表、瀑布流布局、性能优化 |
| Takeout 路由 | takeout-routing.md | One.js 基于文件的路由、SSG/SSR/SPA 模式、动态路由、服务端数据加载 |
| Takeout 身份验证 | takeout-auth.md | Better Auth 集成、会话管理、OAuth、魔法链接、OTP |
| Takeout 数据库 | takeout-database.md | Drizzle ORM 设置、模式定义、类型安全查询、迁移 |
| Takeout Zero | takeout-zero.md | 用于实时数据的 Zero Sync、离线优先架构、乐观更新 |
| 破坏性变更 | breaking-changes-and-new-features.md | 从旧版本迁移指南、配置 v4/v5 差异 |
❌ 错误:
<View padding={16} /> // 硬编码值
<View padding="16px" /> // 带单位的字符串
✅ 正确:
<View padding="$4" /> // 令牌引用
<View padding={16} /> // 如果是故意的,数字也可以 (但不会被编译器提取)
❌ 错误:
// 缺少 `as const`
variants: {
size: {
small: { padding: '$2' },
large: { padding: '$4' },
},
}
✅ 正确:
// 必须使用 `as const` 以获得正确的 TypeScript 推断
variants: {
size: {
small: { padding: '$2' },
large: { padding: '$4' },
},
} as const
❌ 错误:
// 在 styled() 中使用平台检测
const Component = styled(View, {
padding: Platform.OS === 'web' ? 10 : 20, // 不会被提取
})
✅ 正确:
// 使用平台属性修饰符
const Component = styled(View, {
padding: 20,
$platform-web: {
padding: 10,
},
})
❌ 错误:
<View
$gtMd={{ padding: '$8' }} // 首先应用
padding="$4" // 覆盖媒体查询
/>
✅ 正确:
<View
padding="$4" // 基础值
$gtMd={{ padding: '$8' }} // 在中屏幕及以上覆盖
/>
❌ 错误:
// 在 CSS 驱动中使用弹簧物理效果
import { createAnimations } from '@tamagui/animations-css'
const animations = createAnimations({
bouncy: {
type: 'spring', // CSS 不支持弹簧!
damping: 10,
},
})
✅ 正确:
// CSS 驱动使用缓动字符串
import { createAnimations } from '@tamagui/animations-css'
const animations = createAnimations({
bouncy: 'cubic-bezier(0.68, -0.55, 0.265, 1.55) 500ms',
})
// 或者使用 React Native 驱动来支持弹簧
import { createAnimations } from '@tamagui/animations-react-native'
const animations = createAnimations({
bouncy: {
type: 'spring',
damping: 10,
stiffness: 100,
},
})
❌ 错误:
// 创建复合组件但没有使用 withStaticProperties
const Card = styled(View, { context: CardContext })
const CardTitle = styled(Text, { context: CardContext })
// 使用需要单独导入
import { Card } from './Card'
import { CardTitle } from './CardTitle'
<Card>
<CardTitle>标题</CardTitle>
</Card>
✅ 正确:
import { withStaticProperties } from '@tamagui/helpers'
const CardFrame = styled(View, { context: CardContext })
const CardTitle = styled(Text, { context: CardContext })
export const Card = withStaticProperties(CardFrame, {
Title: CardTitle,
})
// 单一导入
<Card>
<Card.Title>标题</Card.Title>
</
Universal React UI framework for web and native (v1.144.0+)
This skill provides comprehensive guidance for building cross-platform React applications using Tamagui's styling system, component library, Bento premium components, and Takeout starter kit.
Tamagui is a universal UI framework that lets you write once and deploy to both web and React Native with optimal performance. It features:
styled() function with design tokens, variants, and responsive patternsVersion: 1.144.0+
Platforms: Web (React), iOS/Android (React Native), Expo
License: Open source (MIT) + Bento/Takeout (commercial licenses)
Activate this skill when encountering these triggers:
Create custom components by extending existing ones:
import { View, Text, styled } from '@tamagui/core'
// Simple styled component
export const Card = styled(View, {
padding: '$4',
backgroundColor: '$background',
borderRadius: '$4',
borderWidth: 1,
borderColor: '$borderColor',
})
// With variants
export const Button = styled(View, {
padding: '$3',
borderRadius: '$2',
backgroundColor: '$blue10',
cursor: 'pointer',
variants: {
variant: {
primary: {
backgroundColor: '$blue10',
},
secondary: {
backgroundColor: '$gray5',
},
outlined: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '$borderColor',
},
},
size: {
small: { padding: '$2' },
medium: { padding: '$3' },
large: { padding: '$4' },
},
} as const,
defaultVariants: {
variant: 'primary',
size: 'medium',
},
})
// Usage
<Button variant="outlined" size="large">Click Me</Button>
Stacks are the foundation of Tamagui layouts:
import { XStack, YStack, ZStack, Text, Button } from 'tamagui'
export function LayoutExample() {
return (
<YStack gap="$4" padding="$4">
{/* Horizontal stack */}
<XStack gap="$2" justifyContent="space-between" alignItems="center">
<Text>Label</Text>
<Button>Action</Button>
</XStack>
{/* Vertical stack with responsive gap */}
<YStack
gap="$4"
$gtSm={{ gap: '$6' }} // Larger gap on small+ screens
>
<Card>Item 1</Card>
<Card>Item 2</Card>
</YStack>
{/* Overlay stack (position: relative) */}
<ZStack width={300} height={200}>
<View backgroundColor="$blue5" fullscreen />
<Text position="absolute" top="$4" left="$4">
Overlay Text
</Text>
</ZStack>
</YStack>
)
}
Design tokens provide consistent values across your app:
import { View, Text, createTamagui } from '@tamagui/core'
// Tokens are defined in createTamagui config
const config = createTamagui({
tokens: {
color: {
white: '#fff',
black: '#000',
blue: '#0066cc',
},
space: {
1: 4,
2: 8,
3: 12,
4: 16,
},
size: {
sm: 100,
md: 200,
lg: 300,
},
radius: {
1: 4,
2: 8,
3: 12,
4: 16,
},
},
})
// Use tokens with $ prefix
<View
padding="$4" // Uses space.4 = 16px
backgroundColor="$blue" // Uses color.blue
borderRadius="$3" // Uses radius.3 = 12px
width="$md" // Uses size.md = 200px
/>
// Tokens work in styled()
const StyledCard = styled(View, {
padding: '$4',
margin: '$2',
backgroundColor: '$background',
borderRadius: '$4',
})
Tamagui uses a semantic color scale system (1-12) for consistent theming:
import { YStack, Text, Theme } from 'tamagui'
export function ThemeExample() {
return (
<YStack
backgroundColor="$color1" // Subtle background
borderColor="$color6" // Regular border
padding="$4"
>
<Text color="$color12">Heading (highest contrast)</Text>
<Text color="$color11">Body text (high contrast)</Text>
<Text color="$color10">Muted text (low contrast)</Text>
{/* Apply sub-theme */}
<Theme name="blue">
<YStack
backgroundColor="$color9" // Blue solid background
padding="$3"
borderRadius="$2"
>
<Text color="$color1">White text on blue</Text>
</Theme>
</YStack>
</YStack>
)
}
// Dynamic theme switching
import { useTheme } from '@tamagui/core'
export function ThemedComponent() {
const theme = useTheme()
// Access theme values directly
console.log(theme.background.val) // e.g., "#ffffff"
console.log(theme.color11.val) // e.g., "#333333"
return <Text color={theme.color11}>Themed Text</Text>
}
Use media query props for responsive layouts:
import { YStack, Text, useMedia } from 'tamagui'
export function ResponsiveExample() {
const media = useMedia()
return (
<YStack
padding="$4"
$gtSm={{ padding: '$6' }} // > sm breakpoint
$gtMd={{ padding: '$8' }} // > md breakpoint
flexDirection="column"
$gtLg={{ flexDirection: 'row' }} // Row layout on large screens
>
<Text
fontSize="$4"
$gtSm={{ fontSize: '$5' }}
$gtMd={{ fontSize: '$6' }}
>
Responsive Text
</Text>
{/* Conditional rendering based on media query */}
{media.gtMd && <Text>Only on medium+ screens</Text>}
</YStack>
)
}
// Configure media queries in createTamagui
const config = createTamagui({
media: {
xs: { maxWidth: 660 },
sm: { maxWidth: 800 },
md: { maxWidth: 1020 },
lg: { maxWidth: 1280 },
xl: { maxWidth: 1420 },
xxl: { maxWidth: 1600 },
gtXs: { minWidth: 660 + 1 },
gtSm: { minWidth: 800 + 1 },
gtMd: { minWidth: 1020 + 1 },
gtLg: { minWidth: 1280 + 1 },
},
})
Add smooth animations to any component:
import { YStack, Button, AnimatePresence } from 'tamagui'
import { useState } from 'react'
export function AnimationExample() {
const [show, setShow] = useState(false)
return (
<YStack gap="$4">
<Button onPress={() => setShow(!show)}>
Toggle
</Button>
<AnimatePresence>
{show && (
<YStack
key="animated-box"
animation="quick"
enterStyle={{
opacity: 0,
y: -20,
scale: 0.9,
}}
exitStyle={{
opacity: 0,
y: 20,
scale: 0.9,
}}
opacity={1}
y={0}
scale={1}
backgroundColor="$blue5"
padding="$4"
borderRadius="$4"
>
<Text>Animated Content</Text>
</YStack>
)}
</AnimatePresence>
</YStack>
)
}
// Define animations in createTamagui
import { createAnimations } from '@tamagui/animations-react-native'
const animations = createAnimations({
quick: {
type: 'spring',
damping: 20,
mass: 1.2,
stiffness: 250,
},
bouncy: {
type: 'spring',
damping: 10,
mass: 0.9,
stiffness: 100,
},
})
Build cohesive component families:
import { createStyledContext, styled, View, Text } from '@tamagui/core'
import { withStaticProperties } from '@tamagui/helpers'
// Define context
const CardContext = createStyledContext({
size: '$4' as any,
})
// Create base component
const CardFrame = styled(View, {
backgroundColor: '$background',
borderRadius: '$4',
borderWidth: 1,
borderColor: '$borderColor',
context: CardContext,
variants: {
size: {
small: { padding: '$3' },
medium: { padding: '$4' },
large: { padding: '$6' },
},
} as const,
})
// Create child components that consume context
const CardTitle = styled(Text, {
context: CardContext,
fontWeight: 'bold',
variants: {
size: {
small: { fontSize: '$4' },
medium: { fontSize: '$5' },
large: { fontSize: '$6' },
},
} as const,
})
const CardDescription = styled(Text, {
context: CardContext,
color: '$color11',
variants: {
size: {
small: { fontSize: '$3' },
medium: { fontSize: '$4' },
large: { fontSize: '$5' },
},
} as const,
})
// Compose with static properties
export const Card = withStaticProperties(CardFrame, {
Title: CardTitle,
Description: CardDescription,
})
// Usage - size cascades to all children
<Card size="large">
<Card.Title>Large Card</Card.Title>
<Card.Description>
All text automatically sized large
</Card.Description>
</Card>
import { Button, Dialog, Input, Label, XStack, YStack } from 'tamagui'
export function LoginForm() {
return (
<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>
<YStack gap="$2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
secureTextEntry
placeholder="••••••••"
/>
</YStack>
<XStack gap="$2" justifyContent="flex-end">
<Button variant="outlined">Cancel</Button>
<Button theme="blue">Sign In</Button>
</XStack>
</YStack>
)
}
import { Button, Dialog, XStack, YStack, H2, Paragraph } from 'tamagui'
export function DialogExample() {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Dialog</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
key="overlay"
animation="quick"
opacity={0.5}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
<Dialog.Content
bordered
elevate
key="content"
animation={['quick', { opacity: { overshootClamping: true } }]}
enterStyle={{ x: 0, y: -20, opacity: 0, scale: 0.9 }}
exitStyle={{ x: 0, y: 10, opacity: 0, scale: 0.95 }}
gap="$4"
>
<Dialog.Title>Confirm Action</Dialog.Title>
<Dialog.Description>
Are you sure you want to proceed?
</Dialog.Description>
<XStack alignSelf="flex-end" gap="$2">
<Dialog.Close displayWhenAdapted asChild>
<Button variant="outlined">Cancel</Button>
</Dialog.Close>
<Dialog.Close asChild>
<Button theme="blue">Confirm</Button>
</Dialog.Close>
</XStack>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
}
import { Button, Dialog, Sheet, Adapt } from 'tamagui'
export function AdaptiveDialog() {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Adapt when="sm" platform="touch">
<Sheet animation="medium" zIndex={200000} modal dismissOnSnapToBottom>
<Sheet.Frame padding="$4" gap="$4">
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay
animation="lazy"
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
</Sheet>
</Adapt>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
{/* Content shows as Dialog on desktop, Sheet on mobile */}
<Dialog.Title>Adaptive UI</Dialog.Title>
<Dialog.Description>
This is a dialog on desktop, sheet on mobile
</Dialog.Description>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
}
Premium components from @tamagui/bento (requires Bento license for production use).
Composable input components with React Hook Form and Zod integration:
import { Input } from '@tamagui/bento/components/inputsParts'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
export function BentoForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
})
return (
<YStack gap="$4">
<Input size="$4">
<Input.Label htmlFor="email">Email</Input.Label>
<Input.Box>
<Input.Section>
<Mail size={16} />
</Input.Section>
<Input.Area
id="email"
placeholder="email@example.com"
{...register('email')}
/>
</Input.Box>
{errors.email && (
<Input.Info color="$red10">{errors.email.message}</Input.Info>
)}
</Input>
<Input size="$4">
<Input.Label htmlFor="password">Password</Input.Label>
<Input.Box>
<Input.Area
id="password"
secureTextEntry
placeholder="••••••••"
{...register('password')}
/>
<Input.Section>
<Input.Button>
<Eye size={16} />
</Input.Button>
</Input.Section>
</Input.Box>
{errors.password && (
<Input.Info color="$red10">{errors.password.message}</Input.Info>
)}
</Input>
</YStack>
)
}
Key Features:
htmlFor bindingFocusContextProduction-ready data tables with sorting, filtering, and pagination:
import { DataTable } from '@tamagui/bento/components/Table'
import { createColumnHelper } from '@tanstack/react-table'
type User = {
id: number
name: string
email: string
role: string
}
const columnHelper = createColumnHelper<User>()
const columns = [
columnHelper.accessor('name', {
header: 'Name',
cell: info => info.getValue(),
}),
columnHelper.accessor('email', {
header: 'Email',
cell: info => info.getValue(),
}),
columnHelper.accessor('role', {
header: 'Role',
cell: info => <Text textTransform="capitalize">{info.getValue()}</Text>,
}),
]
export function UsersTable({ users }: { users: User[] }) {
return (
<DataTable
data={users}
columns={columns}
enableSorting
enableFiltering
enablePagination
pageSize={10}
/>
)
}
Efficient list rendering with FlatList patterns:
import { FlatList } from '@tamagui/bento/components/List'
import { YStack, Text } from 'tamagui'
export function VirtualizedList({ items }: { items: string[] }) {
return (
<FlatList
data={items}
keyExtractor={(item, index) => `${item}-${index}`}
renderItem={({ item }) => (
<YStack padding="$4" borderBottomWidth={1} borderColor="$borderColor">
<Text>{item}</Text>
</YStack>
)}
estimatedItemSize={60}
/>
)
}
Additional Patterns:
Full-stack starter kit from Tamagui Takeout (requires license).
File-based routing with SSG/SSR/SPA modes:
app/
_layout.tsx # Root layout
index.tsx # / (home page)
blog/
[slug].tsx # /blog/my-post (dynamic route)
(auth)/ # Route group (no URL nesting)
login.tsx # /login
signup.tsx # /signup
+not-found.tsx # Custom 404 page
Type-safe navigation:
import { Link, useRouter, useParams } from 'one'
export function Navigation() {
const router = useRouter()
return (
<YStack gap="$2">
{/* Declarative navigation */}
<Link href="/">Home</Link>
<Link href="/blog/hello-world">Blog Post</Link>
{/* Programmatic navigation */}
<Button onPress={() => router.push('/about')}>
Go to About
</Button>
</YStack>
)
}
// Dynamic routes
export function BlogPost() {
const { slug } = useParams<{ slug: string }>()
return <Text>Post: {slug}</Text>
}
Server-side data loading:
import { createLoader } from 'one'
export const loader = createLoader(async (context) => {
const post = await db.query.posts.findFirst({
where: eq(posts.slug, context.params.slug),
})
return { post }
})
export default function BlogPost() {
const { post } = useLoader(loader)
return <Text>{post.title}</Text>
}
Complete authentication system with multiple strategies:
import { signIn, signOut, useSession } from '~/lib/auth'
export function AuthExample() {
const session = useSession()
if (!session.user) {
return (
<YStack gap="$4">
{/* Email/password */}
<Button onPress={() => signIn.email({ email, password })}>
Sign In
</Button>
{/* OAuth */}
<Button onPress={() => signIn.social({ provider: 'github' })}>
Sign in with GitHub
</Button>
{/* Magic link */}
<Button onPress={() => signIn.magicLink({ email })}>
Send Magic Link
</Button>
</YStack>
)
}
return (
<YStack gap="$2">
<Text>Welcome, {session.user.name}</Text>
<Button onPress={() => signOut()}>Sign Out</Button>
</YStack>
)
}
Features:
Type-safe database queries with PostgreSQL:
import { db } from '~/db'
import { posts, users } from '~/db/schema'
import { eq, desc } from 'drizzle-orm'
// Define schema
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
})
// Query
const allPosts = await db.query.posts.findMany({
orderBy: [desc(posts.createdAt)],
with: {
author: true, // Join with users
},
})
// Insert
await db.insert(posts).values({
title: 'My Post',
slug: 'my-post',
content: 'Post content...',
authorId: 1,
})
// Update
await db.update(posts)
.set({ title: 'Updated Title' })
.where(eq(posts.id, 1))
// Delete
await db.delete(posts).where(eq(posts.id, 1))
Offline-first real-time synchronization:
import { useQuery, useMutation } from '@rocicorp/zero/react'
import { z } from '@rocicorp/zero'
// Define schema
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
createdAt: z.number(),
})
export function TodoList() {
// Real-time query (updates automatically)
const [todos] = useQuery(q => q.todos.orderBy('createdAt', 'desc'))
// Optimistic mutation
const [addTodo] = useMutation(async (tx, text: string) => {
await tx.todos.insert({
id: crypto.randomUUID(),
text,
completed: false,
createdAt: Date.now(),
})
})
const [toggleTodo] = useMutation(async (tx, id: string) => {
const todo = await tx.todos.get(id)
if (todo) {
await tx.todos.update({
id,
completed: !todo.completed,
})
}
})
return (
<YStack gap="$2">
{todos.map(todo => (
<XStack key={todo.id} gap="$2">
<Checkbox
checked={todo.completed}
onCheckedChange={() => toggleTodo(todo.id)}
/>
<Text>{todo.text}</Text>
</XStack>
))}
</YStack>
)
}
Features:
For detailed documentation on specific topics, refer to these reference files:
| Topic | Reference File | Description |
|---|---|---|
| Core Styling | core-styling.md | styled() API, variant systems, createStyledContext, composition patterns, TypeScript integration |
| Components | components.md | Button, Dialog, Sheet, Input, Select, Tabs, Switch, Popover, Stacks, Adapt pattern |
| Theming |
❌ Wrong:
<View padding={16} /> // Hard-coded value
<View padding="16px" /> // String with unit
✅ Correct:
<View padding="$4" /> // Token reference
<View padding={16} /> // Number is OK if intentional (not extracted by compiler)
❌ Wrong:
// Missing `as const`
variants: {
size: {
small: { padding: '$2' },
large: { padding: '$4' },
},
}
✅ Correct:
// Must use `as const` for proper TypeScript inference
variants: {
size: {
small: { padding: '$2' },
large: { padding: '$4' },
},
} as const
❌ Wrong:
// Using platform detection in styled()
const Component = styled(View, {
padding: Platform.OS === 'web' ? 10 : 20, // Won't extract
})
✅ Correct:
// Use platform prop modifiers
const Component = styled(View, {
padding: 20,
$platform-web: {
padding: 10,
},
})
❌ Wrong:
<View
$gtMd={{ padding: '$8' }} // Applied first
padding="$4" // Overrides media query
/>
✅ Correct:
<View
padding="$4" // Base value
$gtMd={{ padding: '$8' }} // Overrides on medium+
/>
❌ Wrong:
// Using spring physics with CSS driver
import { createAnimations } from '@tamagui/animations-css'
const animations = createAnimations({
bouncy: {
type: 'spring', // CSS doesn't support springs!
damping: 10,
},
})
✅ Correct:
// CSS driver uses easing strings
import { createAnimations } from '@tamagui/animations-css'
const animations = createAnimations({
bouncy: 'cubic-bezier(0.68, -0.55, 0.265, 1.55) 500ms',
})
// Or use React Native driver for springs
import { createAnimations } from '@tamagui/animations-react-native'
const animations = createAnimations({
bouncy: {
type: 'spring',
damping: 10,
stiffness: 100,
},
})
❌ Wrong:
// Creating compound components without withStaticProperties
const Card = styled(View, { context: CardContext })
const CardTitle = styled(Text, { context: CardContext })
// Usage requires separate imports
import { Card } from './Card'
import { CardTitle } from './CardTitle'
<Card>
<CardTitle>Title</CardTitle>
</Card>
✅ Correct:
import { withStaticProperties } from '@tamagui/helpers'
const CardFrame = styled(View, { context: CardContext })
const CardTitle = styled(Text, { context: CardContext })
export const Card = withStaticProperties(CardFrame, {
Title: CardTitle,
})
// Single import
<Card>
<Card.Title>Title</Card.Title>
</Card>
❌ Wrong:
// Using exitStyle without AnimatePresence
{show && (
<View
animation="quick"
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }} // Won't animate on exit!
/>
)}
✅ Correct:
import { AnimatePresence } from 'tamagui'
<AnimatePresence>
{show && (
<View
key="animated-view" // Key is required!
animation="quick"
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
)}
</AnimatePresence>
❌ Wrong (Performance):
// Inline styles aren't extracted by compiler
{items.map(item => (
<View
key={item.id}
style={{
padding: 16,
backgroundColor: '#fff',
borderRadius: 8,
}}
/>
))}
✅ Correct:
// Use styled() for extractable styles
const ItemView = styled(View, {
padding: '$4',
backgroundColor: '$background',
borderRadius: '$2',
})
{items.map(item => (
<ItemView key={item.id} />
))}
Be aware of platform-specific behaviors:
Styles must be statically analyzable for the compiler to extract them:
❌ Won't Extract:
const dynamicPadding = isPremium ? '$6' : '$4'
<View padding={dynamicPadding} /> // Runtime dynamic value
✅ Will Extract:
<View padding={isPremium ? '$6' : '$4'} /> // Inline ternary is OK
// Or use variants
<View size={isPremium ? 'large' : 'small'} />
When generating Tamagui code, follow these best practices:
Always ensure createTamagui config exists with tokens, themes, and media queries before generating components.
Prefer design tokens ($4, $background, $color11) over hard-coded values for extractable, themeable styles.
Create components with size, variant, and state variants rather than separate component definitions:
// ✅ Good - One component with variants
const Button = styled(View, {
variants: {
variant: { primary: {...}, secondary: {...} },
size: { small: {...}, large: {...} },
} as const,
})
// ❌ Avoid - Multiple separate components
const PrimaryButton = styled(View, { ... })
const SecondaryButton = styled(View, { ... })
For compound components (Card, Input, Select), use createStyledContext to share size and styling:
const Context = createStyledContext({ size: '$4' })
const Parent = styled(View, { context: Context, variants: {...} })
const Child = styled(Text, { context: Context, variants: {...} })
export const Component = withStaticProperties(Parent, { Child })
Always add accessibility props:
<Input
id="email"
aria-label="Email address"
aria-describedby="email-error"
aria-invalid={!!error}
/>
<Label htmlFor="email">Email</Label>
{error && <Text id="email-error">{error}</Text>}
Include responsive props for better UX:
<YStack
padding="$4"
$gtSm={{ padding: '$6' }}
$gtMd={{ padding: '$8' }}
flexDirection="column"
$gtLg={{ flexDirection: 'row' }}
/>
Use enterStyle/exitStyle for enter/exit animations and wrap with AnimatePresence:
<AnimatePresence>
{show && (
<View
key="unique-key"
animation="quick"
enterStyle={{ opacity: 0, y: -10 }}
exitStyle={{ opacity: 0, y: 10 }}
opacity={1}
y={0}
/>
)}
</AnimatePresence>
Use the Adapt pattern for mobile-friendly UIs:
<Dialog>
<Adapt when="sm" platform="touch">
<Sheet>
<Adapt.Contents />
</Sheet>
</Adapt>
<Dialog.Portal>
<Dialog.Content>...</Dialog.Content>
</Dialog.Portal>
</Dialog>
Always export prop types for reusable components:
import { GetProps } from '@tamagui/core'
export const MyComponent = styled(View, { ... })
export type MyComponentProps = GetProps<typeof MyComponent>
When generating test code:
testID prop for component identification (cross-platform)TamaguiProviderAnimatePresence exit callbacksCurrent Version: 1.144.0+
Dependencies:
Ecosystem Packages:
| Package | Description |
|---|---|
tamagui | Meta-package with core + components |
@tamagui/core | Core styling engine |
@tamagui/animations-css | CSS animation driver |
@tamagui/animations-react-native | React Native animation driver |
@tamagui/animations-reanimated | Reanimated v3 driver |
@tamagui/animations-moti |
Migration from v1.x:
breaking-changes-and-new-features.mdFor comprehensive examples and patterns, explore:
Last Updated: January 2026
Skill Version: 2.0
Generated From: Official Tamagui documentation (v1.144.0+)
Weekly Installs
58
Repository
First Seen
Jan 30, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode54
github-copilot53
gemini-cli53
codex51
amp48
kimi-cli47
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
125,600 周安装
theming.md12-step color scale system, theme creation, dynamic theme switching, useTheme hook |
| Animations | animations.md | Animation drivers (CSS, React Native, Reanimated, Motion), enterStyle/exitStyle, AnimatePresence |
| Configuration | configuration.md | createTamagui function, tokens, themes, fonts, media queries, shorthands |
| Compiler | compiler.md | Static extraction, Babel optimization, atomic CSS generation, platform-specific setup |
| Bento Forms | bento-forms.md | React Hook Form integration, Zod validation, composable input system, accessibility |
| Bento Tables | bento-tables.md | TanStack Table v8 integration, sorting, pagination, filtering, responsive layouts |
| Bento Lists | bento-lists.md | FlatList patterns, virtualized lists, masonry layouts, performance optimization |
| Takeout Routing | takeout-routing.md | One.js file-based routing, SSG/SSR/SPA modes, dynamic routes, server-side data loading |
| Takeout Auth | takeout-auth.md | Better Auth integration, session management, OAuth, magic links, OTP |
| Takeout Database | takeout-database.md | Drizzle ORM setup, schema definitions, type-safe queries, migrations |
| Takeout Zero | takeout-zero.md | Zero Sync for real-time data, offline-first architecture, optimistic mutations |
| Breaking Changes | breaking-changes-and-new-features.md | Migration guide from older versions, config v4/v5 differences |
| Moti/Motion driver |
@tamagui/themes | Theme creation helpers |
@tamagui/colors | Radix UI color scales |
@tamagui/bento | Premium components (license required) |
| Tamagui Takeout | Full-stack starter (license required) |