Tamagui
Set up Tamagui for UI in an Expo app with ReadyNative: Tamagui 2 (v5 config, optimising compiler) behind @/ui · Expo Go OK
Expo Go: yes
Runs in Expo Go; no dev build needed for this module.
Tamagui 2.7.7 on the @tamagui/config/v5 preset, as the kit in src/components/ui/ (src/ui/tamagui/ before setup). Pick it when you want a typed styling API with variants and a compiler instead of class strings. JS-only, so Expo Go works; animations use the react-native-reanimated you already have.
Setup
bun run setup --ui tamagui- Set your colours and radius in
readynative.config.ts/src/theme/tokens.ts. bun run gen:theme- regeneratestamagui.config.ts.bun run start -- -c- once, so Metro and Babel pick up the compiler.
Setup installs tamagui, @tamagui/config, @tamagui/animations-reanimated and @tamagui/font-inter (exact 2.7.7) plus the Babel and Metro plugins, and writes babel.config.js (@tamagui/babel-plugin, extraction off in development), metro.config.js (withTamagui) and the generated tamagui.config.ts.
That config is static on purpose - the compiler loads it outside Metro, so no @/ imports. It registers light / dark themes with every colour token as a key ($card, $primaryForeground, $destructiveForeground, $success, $brandAccent…) plus Tamagui's semantic keys (background, color, borderColor…) mapped from them; space and radius extend the v5 scales with the token keys. settings.onlyAllowShorthands is false, so full React Native prop names work. There's no global.css and you never run tamagui generate - TamaguiProvider injects the theme CSS at runtime.
Usage
Screens use the primitives:
import { Box, Button, Screen, Text } from "@/components/ui";
<Screen>
<Box p={4} gap={2}>
<Text variant="title">Inbox</Text>
<Button onPress={refresh}>Refresh</Button>
</Box>
</Screen>;Your own components use styled with $token keys:
import { styled, YStack } from "tamagui";
export const Panel = styled(YStack, {
backgroundColor: "$card",
borderRadius: "$md",
variants: { tone: { danger: { backgroundColor: "$destructive" } } } as const,
});The as const on variants is what types the variant prop.
Inside the kit, RN-backed primitives (Switch, Input's TextInput, Icon, Skeleton, Screen's SafeAreaView) read useTheme(); Tamagui-styled ones use $token keys. ForcedMode is <Theme name="dark"> plus the mode context, so both agree inside the subtree. To add a primitive: add its prop type to src/components/ui/types.ts and export it from src/components/ui/index.ts.
Gotchas
- jest: Tamagui ships ESM
.native.jsentries, sojest.config.jstransformIgnorePatternsincludes|tamagui|@tamagui/.*. No extra mocks - reanimated is already mocked injest.setup.ts. - The compiler writes
.tamagui/at the project root on first build (gitignored).[tamagui] skipped loading 2 module … warning-001a few times is its benign "could not statically load" notice. - Tamagui's
Inputis typed against the web DOM (disabled, noeditable/keyboardType), so the kit'sInputwraps a plain RNTextInputin a Tamagui frame.Switchis the platform switch. @tamagui/font-interis installed but not wired - v5 uses the system fonts. To use Inter, load@tamagui/font-inter/otf/*.otfwithexpo-fontand passcreateInterFont()asfonts.body/fonts.headingin the generator.- React Compiler stays on: tsc, jest and
expo exportpass. Only a device run proves the reanimated press animations; if they misbehave, set"app": { "expo": { "experiments": { "reactCompiler": false } } }inmodule.json. - Web is best-effort:
outputCSSis only honoured by thetamagui generateCLI; runtime CSS injection is what ships. - Core
src/lib/utils.tsstill importsclsx/tailwind-merge(nativewind5 deps); it typechecks only while those linger innode_modules.
Remove it
Every app needs a ui option, so there is no none to fall back to - you switch instead. While modules/ exists (a tree set up with --keep-modules), one call does it:
bun run setup --ui nativewind4 --yes --keep-modulesA finalized tree can't switch: clone a fresh copy of your tier repo, run setup there with the new option and port your code across - see Can I change a module after setup?.
Reference
Everything below is generated from modules/ui/tamagui/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --ui tamaguiModule id: ui/tamagui.
Dependencies
| Package | Version | Kind |
|---|---|---|
@tamagui/animations-reanimated | 2.7.7 | dependency |
@tamagui/config | 2.7.7 | dependency |
@tamagui/font-inter | 2.7.7 | dependency |
tamagui | 2.7.7 | dependency |
@tamagui/babel-plugin | 2.7.7 | devDependency |
@tamagui/metro-plugin | 2.7.7 | devDependency |
Providers
Rendered in src/providers.tsx (lower order = outermost):
| Order | Provider | From |
|---|---|---|
| 50 | ThemeProvider as UiThemeProvider | @/ui |
After setup
- Run
bun run start -- -conce so Metro/Babel pick up the Tamagui compiler.
Files
23 files copied to the project root
babel.config.jsmetro.config.jstamagui.config.tssrc/ui/tamagui/box.tsxsrc/ui/tamagui/button.tsxsrc/ui/tamagui/card.tsxsrc/ui/tamagui/divider.tsxsrc/ui/tamagui/empty-state.tsxsrc/ui/tamagui/error-state.tsxsrc/ui/tamagui/icon.tsxsrc/ui/tamagui/index.tssrc/ui/tamagui/input.tsxsrc/ui/tamagui/list.tsxsrc/ui/tamagui/loading.tsxsrc/ui/tamagui/pressable.tsxsrc/ui/tamagui/row.tsxsrc/ui/tamagui/screen.tsxsrc/ui/tamagui/sheet.tsxsrc/ui/tamagui/skeleton.tsxsrc/ui/tamagui/switch.tsxsrc/ui/tamagui/text.tsxsrc/ui/tamagui/theme-provider.tsxsrc/ui/tamagui/toast.tsx