mobile-ui-components-tamagui
Tamagui universal UI - styled(), tokens, themes, optimizing compiler, responsive media queries, animations, Sheet/Dialog components
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-ui-components-tamagui/skills/mobile-ui-components-tamagui
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Tamagui Universal UI Patterns
Quick Guide: Tamagui provides universal styled components for React Native and web with an optimizing compiler that flattens components to native primitives. Use
styled()with variants for component APIs,$-prefixed tokens for consistent spacing/color, theme nesting for light/dark modes, and thetransitionprop for animations. The compiler extracts static styles to CSS on web and hoists style objects on native -- but only when props are deterministic at build time.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use $-prefixed token values in style props ($4, $color.blue) -- raw pixel values bypass the token system and break theme consistency)
(You MUST keep the transition prop present in JSX when using animations -- conditionally removing it causes expensive hook teardown; pass null to disable instead)
(You MUST add as const to variant definition objects -- without it TypeScript cannot infer variant prop types correctly)
(You MUST use Adapt for responsive Dialog-to-Sheet behavior -- manual Platform.OS branching breaks compiler optimization and misses breakpoint changes)
</critical_requirements>
Auto-detection: Tamagui, tamagui, styled(), createTamagui, createTokens, createTheme, XStack, YStack, ZStack, SizableText, Paragraph, Theme, useTheme, useMedia, $sm, $md, $lg, enterStyle, exitStyle, hoverStyle, pressStyle, transition prop, Sheet, Dialog, Adapt, GetProps, TamaguiProvider, @tamagui/core, @tamagui/config
When to use:
- Building universal React Native + web UIs that share components across platforms
- Creating design-system-driven components with typed token scales and theme variants
- Optimizing render performance via compiler flattening (styled components to native divs/Views)
- Implementing responsive layouts with media query style props (
$sm,$gtMd) - Adding enter/exit/hover/press animations with swappable animation drivers
- Building adaptive overlays (Dialog on desktop, Sheet on mobile) with
Adapt
When NOT to use:
- Web-only projects where a web-native styling solution is simpler
- Apps needing pixel-perfect custom native UI beyond what React Native Views provide
- Performance-critical animations that need direct native driver control beyond Tamagui's animation abstraction
Key patterns covered:
styled()with typed variants (spread, boolean, functional) andGetPropstype extraction- Token system (
createTokens,$-prefix references, category-to-property mapping) - Theme hierarchy (base, sub-themes, component themes,
useTheme, dark/light switching) - Optimizing compiler (flattening, CSS extraction, what prevents optimization)
- Responsive styles with media query props and
useMediahook - Animation system with
transitionprop,enterStyle/exitStyle, and driver selection - Sheet and Dialog with
Adaptfor responsive overlay behavior
Detailed Resources:
- examples/core.md - styled(), variants, tokens, themes, compiler optimization
- examples/responsive-animations.md - Media queries, useMedia, animation drivers, enter/exit styles
- examples/overlays.md - Sheet, Dialog, Adapt pattern
- reference.md - Decision frameworks, token-to-property mapping, migration notes
<decision_framework>
Decision Framework
When to Use styled() vs Inline Props
Is this a reusable component with variants or a semantic name?
├─ YES → styled() with name, variants, defaultVariants
└─ NO → Is this one-off layout?
├─ YES → Inline props on XStack/YStack (<YStack padding="$4">)
└─ NO → styled() if you want component themes or compiler naming
Animation Driver Selection
Platform target?
├─ Web only → @tamagui/animations-css (smallest bundle, CSS transitions)
├─ Native only → @tamagui/animations-reanimated (worklet-based, spring physics)
├─ Universal → Configure per-platform in createTamagui
│ ├─ Web: CSS or Motion driver
│ └─ Native: Reanimated or React Native driver
└─ Simple transitions? → @tamagui/animations-react-native (no extra dependency)
Overlay Component Choice
Need bottom sheet on mobile?
├─ YES → Is there also a desktop version?
│ ├─ YES → Dialog + Adapt + Sheet (single component tree)
│ └─ NO → Sheet standalone
└─ NO → Dialog (portal-based overlay)
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Using raw pixel values (
padding: 16) instead of tokens (padding: "$4") -- bypasses theme system, breaks consistency across platforms - Conditionally removing the
transitionprop from JSX -- causes expensive spring hook teardown/setup; passnullto disable instead - Missing
as conston variant definition objects -- TypeScript infersstringinstead of literal union types, losing autocomplete and type safety - Using
Platform.OSbranching for Dialog vs Sheet -- useAdaptinstead, which responds to breakpoints and is compiler-optimized - Hardcoded color strings (
"#fff","#000") in styled components -- breaks dark/light theme switching; use$background,$color
Medium Priority Issues:
- Not setting
nameon styled components that need component-level themes -- withoutname, Tamagui cannot look up component-specific theme variants likedark_Card - Using JavaScript ternaries for responsive styles instead of media query props -- prevents compiler CSS extraction, adds runtime cost
- Nesting
styled(styled())without.styleable()when wrapping with a functional component -- variant merging breaks silently - Importing from
tamaguiinstead of@tamagui/corewhen you only need the core -- pulls in the entire UI kit unnecessarily
Gotchas & Edge Cases:
- Theme nesting is name-based:
<Theme name="dark"><Theme name="green">resolves todark_green, not justgreen. The sub-theme must be defined asdark_greenin your config. Object.groupByonuseMedia()result fails: The proxied object fromuseMediais not iterable -- use direct key access (media.sm) only.Dialog.Sheetdoes not preserve state when transitioning between Sheet and Portal modes. Lift state above the Dialog if persistence is needed.- Spread variants (
...size) only match top-level token categories -- custom nested token groups require functional variants instead. elevationprop generates both shadow props (iOS) and elevation (Android) on native, but translates tobox-shadowon web. Differences in shadow appearance across platforms are expected.- Config v5 changed flex defaults: With
styleCompat: 'react-native',flexusesflexBasis: 0(notauto). Without it, web defaults apply. - The Moti animation driver is deprecated -- switch to
@tamagui/animations-reanimated(same API, fewer dependencies). - String-to-boolean coercion in config -- if parsing env vars for config flags, the string
"false"is truthy in JavaScript. Use explicit comparison (val === "true") not coercion.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST use $-prefixed token values in style props ($4, $color.blue) -- raw pixel values bypass the token system and break theme consistency)
(You MUST keep the transition prop present in JSX when using animations -- conditionally removing it causes expensive hook teardown; pass null to disable instead)
(You MUST add as const to variant definition objects -- without it TypeScript cannot infer variant prop types correctly)
(You MUST use Adapt for responsive Dialog-to-Sheet behavior -- manual Platform.OS branching breaks compiler optimization and misses breakpoint changes)
Failure to follow these rules will break theme consistency, cause animation performance issues, lose type safety on variants, and produce non-adaptive overlays.
</critical_reminders>
Files (skills)
-
examples
-
core.md 10.8 KB
# Tamagui - Core Patterns > styled() components, variants, tokens, themes, and compiler optimization. See [SKILL.md](../SKILL.md) for decision guidance and red flags. --- ## Pattern 1: styled() with Variants ### Basic Component with Boolean and Spread Variants ```tsx import { GetProps, styled, View, Text } from "@tamagui/core"; export const Card = styled(View, { name: "Card", backgroundColor: "$background", borderRadius: "$4", padding: "$4", borderWidth: 1, borderColor: "$borderColor", variants: { size: { // Spread variant: maps all size tokens automatically "...size": (val, { tokens }) => ({ padding: tokens.size[val] ?? val, borderRadius: tokens.radius[val] ?? val, }), }, elevated: { true: { elevation: "$2", shadowColor: "$shadowColor", }, false: { elevation: "$0", }, }, transparent: { true: { backgroundColor: "transparent", borderWidth: 0, }, }, } as const, // REQUIRED for TypeScript variant inference defaultVariants: { elevated: false, }, }); export type CardProps = GetProps<typeof Card>; ``` **Why good:** `name: "Card"` enables component themes (`dark_Card`), `...size` maps token scale automatically, `as const` preserves literal types, `GetProps` derives props from the styled definition, `defaultVariants` provides typed defaults ### Usage ```tsx <Card size="$4" elevated> <SizableText size="$3">Card content</SizableText> </Card> <Card size="$2" transparent> <SizableText>Transparent card</SizableText> </Card> ``` --- ### Functional Variant with Token Access ```tsx export const Badge = styled(View, { name: "Badge", paddingHorizontal: "$2", paddingVertical: "$1", borderRadius: "$10", variants: { // Functional variant: receives value + utilities status: (val: "success" | "warning" | "error", { theme }) => { const colorMap = { success: theme.green10, warning: theme.yellow10, error: theme.red10, } as const; return { backgroundColor: colorMap[val]?.val, }; }, } as const, }); ``` --- ### Composing styled(styled()) and .styleable() When wrapping a styled component in a functional component (for hooks, logic, etc.), use `.styleable()` to preserve variant merging: ```tsx const StyledButton = styled(View, { name: "Button", backgroundColor: "$background", paddingHorizontal: "$4", paddingVertical: "$2", borderRadius: "$3", alignItems: "center", justifyContent: "center", cursor: "pointer", variants: { variant: { primary: { backgroundColor: "$blue10" }, secondary: { backgroundColor: "$gray5" }, ghost: { backgroundColor: "transparent" }, }, } as const, }); // .styleable() preserves styled() behavior through functional wrapper export const Button = StyledButton.styleable<{ loading?: boolean }>( ({ loading, children, ...props }, ref) => { return ( <StyledButton ref={ref} opacity={loading ? 0.6 : 1} {...props}> {loading ? <Spinner /> : children} </StyledButton> ); }, ); // Further composition still works export const PrimaryButton = styled(Button, { variant: "primary", }); ``` **Why good:** `.styleable()` maintains the styled component contract (variants, theme lookups, compiler optimization) through the functional wrapper. Without it, `styled(Button, ...)` cannot merge variants. --- ### Semantic HTML via render Prop (Web) ```tsx // String render: compiler-optimized, outputs <button> instead of <div> export const NativeButton = styled(View, { render: "button", tag: "button", cursor: "pointer", padding: "$3", borderRadius: "$2", backgroundColor: "$background", }); // Runtime override <Card render="article"> <SizableText>Semantic article card</SizableText> </Card>; ``` **Why good:** string `render` prop is compiler-optimized (unlike function render), produces semantic HTML for accessibility, no deoptimization penalty --- ## Pattern 2: Token System ### Defining Tokens with createTokens ```tsx import { createTokens } from "tamagui"; export const tokens = createTokens({ size: { 0: 0, 1: 4, 2: 8, 3: 12, 4: 16, 5: 20, 6: 24, 8: 32, 10: 40, true: 16, // default size when no value specified }, space: { 0: 0, 1: 4, 2: 8, 3: 12, 4: 16, 5: 20, 6: 24, 8: 32, true: 16, "-1": -4, "-2": -8, // negative space for overlaps }, radius: { 0: 0, 1: 4, 2: 8, 3: 12, 4: 16, 10: 9999, // pill shape true: 8, }, zIndex: { 0: 0, 1: 100, 2: 200, 5: 500, }, color: { white: "#fff", black: "#000", gray1: "#f8f8f8", gray5: "#999", gray10: "#333", }, }); ``` **Key points:** numeric keys (1-10) recommended for Tamagui UI kit compatibility, `true` key provides default when no size specified, negative space tokens enable overlap patterns, all keys auto-prefixed with `$` when used in components. ### Token Access in Components ```tsx // In JSX -- $ prefix resolves from tokens/theme <YStack padding="$4" gap="$2" backgroundColor="$background"> <SizableText size="$5">Token-driven layout</SizableText> </YStack>; // Programmatic access import { getTokens } from "@tamagui/core"; const size4 = getTokens().size[4].val; // 16 const sizeVar = getTokens().size[4].variable; // "--size-4" (CSS variable) ``` --- ## Pattern 3: Theme Definition and Switching ### Creating Themes ```tsx import { createTheme } from "tamagui"; const lightTheme = createTheme({ background: "#fff", backgroundHover: "#f5f5f5", backgroundPress: "#eee", color: "#000", colorHover: "#333", borderColor: "#ddd", borderColorHover: "#ccc", shadowColor: "rgba(0,0,0,0.1)", placeholderColor: "#999", }); const darkTheme = createTheme({ background: "#111", backgroundHover: "#1a1a1a", backgroundPress: "#222", color: "#fff", colorHover: "#eee", borderColor: "#333", borderColorHover: "#444", shadowColor: "rgba(0,0,0,0.5)", placeholderColor: "#666", }); // Sub-themes: dark_blue inherits from dark, overrides specific keys const darkBlueTheme = createTheme({ ...darkTheme, background: "#0a1628", borderColor: "#1a3a5c", }); ``` ### Using Themes in the App ```tsx import { Theme, useTheme, useThemeName } from "tamagui"; // Wrap sections to apply themes function App() { return ( <Theme name="dark"> <YStack flex={1} backgroundColor="$background"> <Header /> <Theme name="blue"> {/* Resolves to dark_blue theme */} <FeatureSection /> </Theme> </YStack> </Theme> ); } // Access theme values programmatically function CustomComponent() { const theme = useTheme(); const themeName = useThemeName(); // "dark", "dark_blue", etc. return ( <YStack backgroundColor={theme.background.val}> <SizableText>Current theme: {themeName}</SizableText> </YStack> ); } ``` **Why good:** theme nesting composes names automatically, `useTheme()` provides typed access to all theme values, sub-themes only need to override changed keys --- ## Pattern 4: Configuration with createTamagui ### Minimal Config ```tsx import { createTamagui, createTokens, createFont } from "tamagui"; const bodyFont = createFont({ family: "Inter, Helvetica, Arial, sans-serif", size: { 1: 12, 2: 14, 3: 16, 4: 18, 5: 22, 6: 28 }, lineHeight: { 1: 18, 2: 20, 3: 24, 4: 26, 5: 30, 6: 36 }, weight: { 1: "400", 3: "600", 5: "700" }, letterSpacing: { 1: 0, 3: -0.5 }, }); export const config = createTamagui({ tokens, themes: { light: lightTheme, dark: darkTheme, dark_blue: darkBlueTheme, }, fonts: { body: bodyFont, heading: bodyFont, // or a separate heading font }, media: { sm: { maxWidth: 640 }, gtSm: { minWidth: 641 }, md: { maxWidth: 768 }, gtMd: { minWidth: 769 }, lg: { maxWidth: 1024 }, gtLg: { minWidth: 1025 }, short: { maxHeight: 820 }, hoverable: { hover: "hover" }, touchable: { pointer: "coarse" }, }, shorthands: { px: "paddingHorizontal", py: "paddingVertical", mx: "marginHorizontal", my: "marginVertical", f: "flex", w: "width", h: "height", bg: "backgroundColor", br: "borderRadius", } as const, }); // Type augmentation for full IDE support type AppConfig = typeof config; declare module "tamagui" { interface TamaguiCustomConfig extends AppConfig {} } ``` ### Using Default Config as Starting Point ```tsx import { defaultConfig } from "@tamagui/config/v5"; import { createTamagui } from "tamagui"; export const config = createTamagui({ ...defaultConfig, // Override only what you need themes: { ...defaultConfig.themes, // Add custom themes dark_brand: createTheme({ ...defaultConfig.themes.dark, background: "#0a0a2e", }), }, }); ``` **Why good:** `@tamagui/config/v5` provides Tailwind-aligned breakpoints, Radix Colors v3, pre-built animation presets, and a complete theme system out of the box --- ## Pattern 5: Compiler Optimization Guide ### What Gets Flattened The compiler performs partial evaluation, analyzing styled() calls and JSX usage to flatten components to native primitives. ```tsx // BEFORE compilation (developer code) const MyCard = styled(View, { name: "MyCard", backgroundColor: "$background", padding: "$4", borderRadius: "$3", }); <MyCard elevated />; // AFTER compilation (web output) // MyCard → <div class="bg-[var(--background)] p-4 rounded-3" /> // Atomic CSS extracted, component tree flattened ``` ### Optimizable vs Non-Optimizable Patterns ```tsx // OPTIMIZABLE: static token, compiler extracts to CSS <YStack padding="$4" backgroundColor="$background" /> // OPTIMIZABLE: media query props become @media rules <YStack padding="$2" $gtMd={{ padding: "$4" }} /> // OPTIMIZABLE: boolean variant with static value <Card elevated /> // OPTIMIZABLE: spread variant with token value <Card size="$4" /> // NOT OPTIMIZABLE: runtime ternary <YStack padding={isExpanded ? "$6" : "$2"} /> // Fix: use media queries if screen-size-dependent, // or accept runtime cost if truly dynamic // NOT OPTIMIZABLE: function render prop <Card render={(props, state) => <CustomCard {...props} />} /> // Fix: use string render prop or accept deoptimization // NOT OPTIMIZABLE: spread from runtime object <Card {...dynamicStyles} /> ``` ### Build Configuration ```tsx // tamagui.build.ts import type { TamaguiBuildOptions } from "tamagui"; export default { config: "./tamagui.config.ts", components: ["tamagui"], outputCSS: "./public/tamagui.generated.css", disableExtraction: process.env.NODE_ENV === "development", } satisfies TamaguiBuildOptions; ``` **Key points:** compiler is optional (Tamagui works at runtime without it), recommended for production only, generates output in `.tamagui/` (add to `.gitignore`), `disableExtraction` recommended during development for faster HMR. -
overlays.md 8.6 KB
# Tamagui - Sheet, Dialog, and Adapt > Overlay components and responsive adaptation. See [SKILL.md](../SKILL.md) for decision guidance. --- ## Pattern 1: Controlled Sheet with Snap Points Sheet slides up from the bottom. Use snap points to define positions, and `dismissOnSnapToBottom` for swipe-to-close. ```tsx import { useState } from "react"; import { Button, Sheet, YStack, SizableText } from "tamagui"; const SNAP_POINTS = [85, 50, 25] as const; const INITIAL_POSITION = 0; function BottomSheet() { const [open, setOpen] = useState(false); const [position, setPosition] = useState(INITIAL_POSITION); return ( <> <Button onPress={() => setOpen(true)}>Open Sheet</Button> <Sheet open={open} onOpenChange={setOpen} snapPoints={SNAP_POINTS} snapPointsMode="percent" position={position} onPositionChange={setPosition} dismissOnSnapToBottom modal > <Sheet.Overlay /> <Sheet.Handle /> <Sheet.Frame padding="$4"> <YStack gap="$3"> <SizableText size="$6">Sheet Title</SizableText> <SizableText size="$3" color="$gray10"> Swipe down to dismiss or snap to different positions. </SizableText> </YStack> </Sheet.Frame> </Sheet> </> ); } ``` **Key points:** `snapPoints` array values go from most visible (85%) to least visible (25%), `snapPointsMode="percent"` interprets values as screen percentage, `dismissOnSnapToBottom` closes when swiped past last snap point, `modal` adds overlay and prevents background interaction. --- ## Pattern 2: Sheet with Scrollable Content Use `Sheet.ScrollView` instead of a regular ScrollView inside Sheet for proper gesture coordination. ```tsx function ScrollableSheet({ items }: { items: string[] }) { const [open, setOpen] = useState(false); return ( <Sheet open={open} onOpenChange={setOpen} snapPoints={[80]} dismissOnSnapToBottom modal > <Sheet.Overlay /> <Sheet.Handle /> <Sheet.Frame> <YStack padding="$4" paddingBottom="$0"> <SizableText size="$5">Scrollable Content</SizableText> </YStack> {/* Sheet.ScrollView coordinates gestures with sheet drag */} <Sheet.ScrollView padding="$4"> <YStack gap="$2"> {items.map((item) => ( <YStack key={item} padding="$3" backgroundColor="$background" borderRadius="$2" > <SizableText>{item}</SizableText> </YStack> ))} </YStack> </Sheet.ScrollView> </Sheet.Frame> </Sheet> ); } ``` **Why `Sheet.ScrollView`:** a regular ScrollView captures all vertical gestures, preventing the sheet from being dragged down. `Sheet.ScrollView` coordinates scroll and drag gestures so the sheet dismisses when scrolled to top. --- ## Pattern 3: Dialog with Portal Dialog renders content in a portal above the rest of the app. Use sub-components for semantic structure. ```tsx import { Button, Dialog, YStack, SizableText, XStack } from "tamagui"; function ConfirmDialog() { return ( <Dialog modal> <Dialog.Trigger asChild> <Button>Delete Item</Button> </Dialog.Trigger> <Dialog.Portal> <Dialog.Overlay key="overlay" transition="quick" enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} opacity={0.5} /> <Dialog.Content key="content" transition="quick" enterStyle={{ opacity: 0, scale: 0.95, y: -10 }} exitStyle={{ opacity: 0, scale: 0.95, y: -10 }} bordered elevate padding="$4" gap="$3" > <Dialog.Title>Confirm Deletion</Dialog.Title> <Dialog.Description> This action cannot be undone. Are you sure? </Dialog.Description> <XStack gap="$3" justifyContent="flex-end"> <Dialog.Close asChild> <Button>Cancel</Button> </Dialog.Close> <Dialog.Close asChild> <Button theme="red">Delete</Button> </Dialog.Close> </XStack> </Dialog.Content> </Dialog.Portal> </Dialog> ); } ``` **Key points:** `asChild` on Trigger/Close makes the child element the actual interactive element, `key` props on Overlay and Content enable AnimatePresence exit animations, `bordered elevate` adds border and shadow from theme. --- ## Pattern 4: Dialog with Adapt (Sheet on Mobile) The primary pattern for responsive overlays. `Adapt` renders Dialog.Content as a Sheet at smaller breakpoints on touch devices. ```tsx import { Adapt, Button, Dialog, Sheet, YStack, SizableText, XStack, Input, } from "tamagui"; function AdaptiveFormDialog() { return ( <Dialog modal> <Dialog.Trigger asChild> <Button>Edit Profile</Button> </Dialog.Trigger> {/* On small touch screens, render as bottom sheet */} <Adapt when="sm" platform="touch"> <Sheet modal dismissOnSnapToBottom snapPoints={[85]}> <Sheet.Frame padding="$4" gap="$4"> {/* Adapt.Contents inserts Dialog.Content children here */} <Adapt.Contents /> </Sheet.Frame> <Sheet.Overlay /> </Sheet> </Adapt> {/* On larger screens / non-touch, render as centered dialog */} <Dialog.Portal> <Dialog.Overlay key="overlay" transition="quick" enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} opacity={0.5} /> <Dialog.Content key="content" bordered elevate transition="quick" enterStyle={{ opacity: 0, scale: 0.95 }} exitStyle={{ opacity: 0, scale: 0.95 }} padding="$4" gap="$4" width={400} > <Dialog.Title>Edit Profile</Dialog.Title> <Dialog.Description> Update your display name and bio. </Dialog.Description> <YStack gap="$3"> <Input placeholder="Display name" /> <Input placeholder="Bio" /> </YStack> <XStack gap="$3" justifyContent="flex-end"> <Dialog.Close asChild> <Button>Cancel</Button> </Dialog.Close> <Dialog.Close asChild> <Button theme="active">Save</Button> </Dialog.Close> </XStack> </Dialog.Content> </Dialog.Portal> </Dialog> ); } ``` **Why good:** single component tree handles both desktop dialog and mobile sheet, `Adapt.Contents` injects the Dialog.Content children into Sheet.Frame, breakpoint-driven (`when="sm"`) responds to viewport changes (not just initial platform). **Gotcha:** `Dialog.Sheet` does NOT preserve state when transitioning between Sheet and Portal modes. If your form has input state, lift it above the Dialog component to avoid state loss during adaptation transitions. --- ## Pattern 5: Controlled Dialog with External State For dialogs that need external open/close control (e.g., from a parent component or store). ```tsx function ControlledDialog({ open, onOpenChange, }: { open: boolean; onOpenChange: (open: boolean) => void; }) { return ( <Dialog open={open} onOpenChange={onOpenChange} modal> <Dialog.Portal> <Dialog.Overlay key="overlay" transition="quick" enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} opacity={0.5} /> <Dialog.Content key="content" transition="medium" enterStyle={{ opacity: 0, y: -20 }} exitStyle={{ opacity: 0, y: -20 }} padding="$4" gap="$4" bordered elevate > <Dialog.Title>Controlled Dialog</Dialog.Title> <Dialog.Description>Open state managed by parent.</Dialog.Description> <Dialog.Close asChild> <Button>Close</Button> </Dialog.Close> </Dialog.Content> </Dialog.Portal> </Dialog> ); } // Usage: parent manages state function Parent() { const [dialogOpen, setDialogOpen] = useState(false); return ( <> <Button onPress={() => setDialogOpen(true)}>Open</Button> <ControlledDialog open={dialogOpen} onOpenChange={setDialogOpen} /> </> ); } ``` **Performance tip:** If Dialog is inside a frequently re-rendering list, place only `Dialog.Trigger` inside the list item and lift the Dialog itself to a parent component. Dialog has significant sub-component overhead that should not be multiplied per list item. -
responsive-animations.md 7 KB
# Tamagui - Responsive Styles and Animations > Media queries, useMedia hook, animation drivers, and enter/exit animations. See [SKILL.md](../SKILL.md) for decision guidance. --- ## Pattern 1: Responsive Styles with Media Query Props Media query props use `$`-prefixed breakpoint names. Base styles apply at all sizes (mobile-first), overridden progressively at larger breakpoints. ```tsx import { XStack, YStack, SizableText } from "tamagui"; function ResponsiveLayout() { return ( <XStack flexDirection="column" padding="$2" gap="$2" $gtSm={{ flexDirection: "row", padding: "$4", gap: "$4" }} $gtLg={{ padding: "$6", gap: "$6" }} > <YStack flex={1} backgroundColor="$background" padding="$3" borderRadius="$3" > <SizableText size="$4">Panel A</SizableText> </YStack> <YStack flex={1} backgroundColor="$background" padding="$3" borderRadius="$3" > <SizableText size="$4">Panel B</SizableText> </YStack> </XStack> ); } ``` **Why good:** mobile-first base styles, `$gtSm` and `$gtLg` override progressively, compiler extracts to CSS @media rules on web (zero JS runtime cost) --- ## Pattern 2: useMedia Hook for Conditional Logic When you need media state in JavaScript logic (not just styles), use `useMedia`. The compiler extracts this to CSS when all usages are deterministic. ```tsx import { useMedia, YStack, SizableText } from "tamagui"; function AdaptiveContent() { const media = useMedia(); return ( <YStack padding="$4"> <SizableText size={media.sm ? "$3" : "$5"}> {media.sm ? "Mobile view" : "Desktop view"} </SizableText> {/* Conditional rendering based on breakpoint */} {!media.sm && ( <YStack> <SizableText>Desktop-only sidebar content</SizableText> </YStack> )} </YStack> ); } ``` **Gotcha:** the `useMedia()` return object is a Proxy -- you cannot use `Object.keys()`, `for...in`, or the `in` operator on it. Access keys directly: `media.sm`, `media.gtMd`. --- ## Pattern 3: Animation Driver Configuration Animation drivers are configured in `createTamagui` and swapped per platform. Component code stays identical. ### CSS Driver (Web -- Smallest Bundle) ```tsx import { createAnimations } from "@tamagui/animations-css"; const animations = createAnimations({ bouncy: "ease-in 200ms", lazy: "ease-out 600ms", quick: "ease-in-out 100ms", medium: "ease-in-out 300ms", }); ``` ### Reanimated Driver (Native -- Spring Physics) ```tsx import { createAnimations } from "@tamagui/animations-reanimated"; const BOUNCY_DAMPING = 10; const BOUNCY_STIFFNESS = 100; const LAZY_DAMPING = 18; const LAZY_STIFFNESS = 50; const QUICK_DAMPING = 20; const QUICK_STIFFNESS = 250; const animations = createAnimations({ bouncy: { damping: BOUNCY_DAMPING, mass: 0.9, stiffness: BOUNCY_STIFFNESS, }, lazy: { damping: LAZY_DAMPING, stiffness: LAZY_STIFFNESS, }, quick: { damping: QUICK_DAMPING, mass: 1.2, stiffness: QUICK_STIFFNESS, }, }); ``` ### Platform-Specific Driver Selection ```tsx import { createTamagui } from "tamagui"; export const config = createTamagui({ // ... tokens, themes, media animations, // Use CSS on web, Reanimated on native via separate configs }); ``` **Key points:** define the same animation names across drivers for cross-platform compatibility. CSS driver uses easing strings, Reanimated uses spring physics objects. The `@tamagui/animations-moti` driver is deprecated -- use `@tamagui/animations-reanimated` instead (same API, fewer deps). --- ## Pattern 4: Transition Prop and Animation States The `transition` prop activates animations on a component. Combine with `enterStyle`, `exitStyle`, `hoverStyle`, `pressStyle`, and `focusStyle` for declarative state-based animations. ### Mount Animation with enterStyle ```tsx import { YStack, SizableText } from "tamagui"; function FadeInCard() { return ( <YStack transition="bouncy" enterStyle={{ opacity: 0, scale: 0.95, y: -10 }} opacity={1} scale={1} y={0} backgroundColor="$background" padding="$4" borderRadius="$4" > <SizableText>Fades and scales in on mount</SizableText> </YStack> ); } ``` **Why good:** SSR-safe (renders base styles on server, animates on client), declarative mount animation without useEffect ### Interaction Animations ```tsx function InteractiveCard() { return ( <YStack transition="quick" hoverStyle={{ scale: 1.02, backgroundColor: "$backgroundHover" }} pressStyle={{ scale: 0.98, backgroundColor: "$backgroundPress" }} focusStyle={{ borderColor: "$borderColorFocus", borderWidth: 2 }} backgroundColor="$background" padding="$4" borderRadius="$4" cursor="pointer" > <SizableText>Hover and press me</SizableText> </YStack> ); } ``` ### Controlling Which Properties Animate ```tsx <YStack transition="bouncy" animateOnly={["transform"]} hoverStyle={{ scale: 1.05, backgroundColor: "$backgroundHover" }} > {/* Only scale (transform) animates; backgroundColor changes instantly */} </YStack> ``` --- ## Pattern 5: AnimatePresence for Exit Animations `AnimatePresence` enables exit animations when components unmount. Wrap conditional content and provide `exitStyle`. ```tsx import { AnimatePresence } from "tamagui"; function ToastNotification({ visible, message, }: { visible: boolean; message: string; }) { return ( <AnimatePresence> {visible && ( <YStack key="toast" transition="quick" enterStyle={{ opacity: 0, y: -20 }} exitStyle={{ opacity: 0, y: -20 }} opacity={1} y={0} backgroundColor="$background" padding="$3" borderRadius="$3" > <SizableText>{message}</SizableText> </YStack> )} </AnimatePresence> ); } ``` **Key points:** `key` prop is required on animated children inside `AnimatePresence`, `exitStyle` defines the animation target on unmount, the animation reverses the enterStyle path by default. --- ## Pattern 6: Disabling Animations Safely Never conditionally remove the `transition` prop from JSX. Spring-based drivers allocate expensive hooks when the prop exists in the props object. ```tsx // GOOD: pass null to disable -- no hook teardown <YStack transition={isAnimating ? "bouncy" : null}> <SizableText>Content</SizableText> </YStack> // BAD: conditional prop inclusion causes hook teardown/setup <YStack {...(isAnimating && { transition: "bouncy" })}> <SizableText>Content</SizableText> </YStack> ``` **Why bad:** conditionally spreading the `transition` prop causes the animation driver hooks to mount/unmount on every toggle, which is expensive and can cause visual glitches. Passing `null` keeps the hook mounted but inactive. If you need to change the animation name after mount, update the component's `key` prop to force a clean remount.
-
-
reference.md 7.1 KB
# Tamagui Quick Reference > Decision frameworks, token mapping, and checklists. See [SKILL.md](SKILL.md) for patterns and red flags. --- ## Token Category to Style Property Mapping | Token Category | Applied To | | -------------- | ------------------------------------------------------- | | **size** | width, height, minWidth, maxWidth, minHeight, maxHeight | | **space** | margin, padding, gap, top, left, right, bottom | | **radius** | borderRadius, borderTopLeftRadius, etc. | | **color** | color, backgroundColor, borderColor, shadowColor | | **zIndex** | zIndex | | **font** | fontFamily (via createFont) | Access tokens in components with `$` prefix: `<YStack padding="$4" backgroundColor="$background" />` Access tokens programmatically: `getTokens().size.small.val` (raw value), `getTokens().size.small.variable` (CSS variable) --- ## Standard Theme Keys Themes should define these keys for full UI kit compatibility: | Key | Purpose | | ------------------------------------------------------------ | --------------------------- | | `background` | Component/screen background | | `backgroundHover` | Hover state background | | `backgroundPress` | Press state background | | `backgroundFocus` | Focus state background | | `color` | Primary text color | | `colorHover` / `colorPress` / `colorFocus` | Text state variants | | `borderColor` | Default border color | | `borderColorHover` / `borderColorPress` / `borderColorFocus` | Border state variants | | `shadowColor` | Shadow color | | `placeholderColor` | Input placeholder text | | `outlineColor` | Focus outline | --- ## Theme Naming Convention ``` base: light, dark sub-theme: dark_green, light_blue multi-level: dark_green_subtle component: dark_Card, light_Button ``` Nesting resolves automatically: `<Theme name="dark"><Theme name="green">` looks up `dark_green`. --- ## Media Query Config (v5 defaults, Tailwind-aligned) | Name | Breakpoint | Type | | ------- | -------------- | --------- | | `$sm` | maxWidth: 640 | max-width | | `$md` | maxWidth: 768 | max-width | | `$lg` | maxWidth: 1024 | max-width | | `$xl` | maxWidth: 1280 | max-width | | `$xxl` | maxWidth: 1536 | max-width | | `$gtSm` | minWidth: 641 | min-width | | `$gtMd` | minWidth: 769 | min-width | | `$gtLg` | minWidth: 1025 | min-width | Additional: `$short` (maxHeight), `$pointerTouch` (touch device), `$hoverable` (hover capable) --- ## Animation Drivers | Driver | Package | Best For | | ------------ | ---------------------------------- | --------------------------------------------- | | CSS | `@tamagui/animations-css` | Web (smallest bundle, native CSS transitions) | | React Native | `@tamagui/animations-react-native` | Native (no extra deps, Animated API) | | Reanimated | `@tamagui/animations-reanimated` | Native (worklet-based, spring physics) | | Motion | `@tamagui/animations-motion` | Web (WAAPI, advanced web animations) | **Deprecated:** `@tamagui/animations-moti` -- switch to `@tamagui/animations-reanimated` (same API). --- ## Compiler Optimization Checklist Styles that **CAN** be flattened (compiler extracts to CSS/static styles): - [x] Token values: `padding="$4"`, `color="$color"` - [x] Spread variants: `size="$4"` with `"...size"` variant - [x] Boolean variants: `elevated` / `elevated={true}` - [x] Media query props: `$gtMd={{ padding: "$4" }}` - [x] String render prop: `render="button"` (web semantic HTML) - [x] Static defaultVariants Styles that **CANNOT** be flattened (runtime evaluation required): - [ ] JavaScript ternaries: `padding={isLarge ? "$4" : "$2"}` - [ ] Function render props: `render={(props) => <Custom {...props} />}` - [ ] Dynamic expressions in style values - [ ] Conditionally applied props via spread: `{...conditionalStyles}` - [ ] Runtime-computed variant values --- ## Spread Variant Categories Available for `"...category"` syntax in variant definitions: `...size`, `...color`, `...radius`, `...space`, `...font`, `...fontSize`, `...lineHeight`, `...letterSpace`, `...zIndex` Functional variant receives: `(value, { tokens, theme, props, font, fontFamily, fonts, context })` --- ## Stack Components | Component | Layout | Equivalent | | --------- | ------------------ | ------------------------------------- | | `XStack` | Horizontal (row) | `View` with `flexDirection: 'row'` | | `YStack` | Vertical (column) | `View` with `flexDirection: 'column'` | | `ZStack` | Layered (absolute) | Children absolutely positioned | All extend `View` from `@tamagui/core` and accept all style properties. --- ## Text Components | Component | Purpose | | ------------- | --------------------------------------------------------------------- | | `Text` | Base text, no theme defaults | | `SizableText` | Text with `size` prop (maps to font scale) | | `Paragraph` | SizableText with semantic `<p>` on web, default size/color from theme | --- ## v2 Migration Notes (from v1) | v1 | v2 | Notes | | ------------------------------------ | ------------------------------------------------- | ---------------------------------------- | | `animation="bouncy"` | `transition="bouncy"` | Prop renamed | | `@tamagui/config/v4` | `@tamagui/config/v5` | New defaults, Tailwind breakpoints | | `@tamagui/animations-moti` | `@tamagui/animations-reanimated` | Moti deprecated | | `2xl` / `2xs` media names | `xxl` / `xxs` | Kebab-case standardized | | `flex` defaults to `flexBasis: auto` | `flexBasis: 0` with `styleCompat: 'react-native'` | Check layouts | | `defaultPosition: 'relative'` | `static` (browser default) | Explicit `position="relative"` if needed | | Animations bundled in config | Import separately (`v5-css`, `v5-rn`) | Smaller default bundle | -
SKILL.md 15.6 KB
--- name: mobile-ui-components-tamagui description: Tamagui universal UI - styled(), tokens, themes, optimizing compiler, responsive media queries, animations, Sheet/Dialog components --- # Tamagui Universal UI Patterns > **Quick Guide:** Tamagui provides universal styled components for React Native and web with an optimizing compiler that flattens components to native primitives. Use `styled()` with variants for component APIs, `$`-prefixed tokens for consistent spacing/color, theme nesting for light/dark modes, and the `transition` prop for animations. The compiler extracts static styles to CSS on web and hoists style objects on native -- but only when props are deterministic at build time. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `$`-prefixed token values in style props (`$4`, `$color.blue`) -- raw pixel values bypass the token system and break theme consistency)** **(You MUST keep the `transition` prop present in JSX when using animations -- conditionally removing it causes expensive hook teardown; pass `null` to disable instead)** **(You MUST add `as const` to variant definition objects -- without it TypeScript cannot infer variant prop types correctly)** **(You MUST use `Adapt` for responsive Dialog-to-Sheet behavior -- manual Platform.OS branching breaks compiler optimization and misses breakpoint changes)** </critical_requirements> --- **Auto-detection:** Tamagui, tamagui, styled(), createTamagui, createTokens, createTheme, XStack, YStack, ZStack, SizableText, Paragraph, Theme, useTheme, useMedia, $sm, $md, $lg, enterStyle, exitStyle, hoverStyle, pressStyle, transition prop, Sheet, Dialog, Adapt, GetProps, TamaguiProvider, @tamagui/core, @tamagui/config **When to use:** - Building universal React Native + web UIs that share components across platforms - Creating design-system-driven components with typed token scales and theme variants - Optimizing render performance via compiler flattening (styled components to native divs/Views) - Implementing responsive layouts with media query style props (`$sm`, `$gtMd`) - Adding enter/exit/hover/press animations with swappable animation drivers - Building adaptive overlays (Dialog on desktop, Sheet on mobile) with `Adapt` **When NOT to use:** - Web-only projects where a web-native styling solution is simpler - Apps needing pixel-perfect custom native UI beyond what React Native Views provide - Performance-critical animations that need direct native driver control beyond Tamagui's animation abstraction **Key patterns covered:** - `styled()` with typed variants (spread, boolean, functional) and `GetProps` type extraction - Token system (`createTokens`, `$`-prefix references, category-to-property mapping) - Theme hierarchy (base, sub-themes, component themes, `useTheme`, dark/light switching) - Optimizing compiler (flattening, CSS extraction, what prevents optimization) - Responsive styles with media query props and `useMedia` hook - Animation system with `transition` prop, `enterStyle`/`exitStyle`, and driver selection - Sheet and Dialog with `Adapt` for responsive overlay behavior **Detailed Resources:** - [examples/core.md](examples/core.md) - styled(), variants, tokens, themes, compiler optimization - [examples/responsive-animations.md](examples/responsive-animations.md) - Media queries, useMedia, animation drivers, enter/exit styles - [examples/overlays.md](examples/overlays.md) - Sheet, Dialog, Adapt pattern - [reference.md](reference.md) - Decision frameworks, token-to-property mapping, migration notes --- <philosophy> ## Philosophy Tamagui solves the universal UI problem: write components once that render optimally on both React Native and web. The key insight is that **compile-time analysis can eliminate the runtime cost of a universal abstraction**. **Core principles:** 1. **Tokens are the source of truth** -- spacing, color, radius, and font values flow from `createTokens` through themes to components via `$`-prefixed references. Raw values bypass the system. 2. **Themes override tokens contextually** -- themes are scoped CSS variables that change within React subtrees. Missing theme keys resolve upward to parent themes, then to tokens. 3. **The compiler rewards deterministic styles** -- static props, token references, and spread variants can be flattened to native primitives. Dynamic expressions, ternaries on non-media values, and function render props prevent optimization. 4. **Animation drivers are swappable** -- CSS transitions for web, Reanimated for native, configured once in `createTamagui`. Component code stays identical. 5. **Adapt replaces platform branching** -- `Adapt` transforms Dialog to Sheet at breakpoints without manual `Platform.OS` checks. **Tamagui v2** is the current stable release. Key changes from v1: `animation` prop renamed to `transition`, config v5 with Tailwind-aligned breakpoints, expanded color system with Radix Colors v3, native portals for Sheet/Dialog/Popover, and headless/unstyled component variants. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: styled() with Typed Variants `styled()` extends a base component with default styles and type-safe variants. Use `GetProps` to export the derived prop type. ```tsx import { GetProps, styled, View, Text } from "@tamagui/core"; export const Card = styled(View, { name: "Card", backgroundColor: "$background", borderRadius: "$4", padding: "$4", borderWidth: 1, borderColor: "$borderColor", variants: { size: { "...size": (val, { tokens }) => ({ padding: tokens.size[val] ?? val, }), }, elevated: { true: { elevation: "$2" }, }, } as const, }); export type CardProps = GetProps<typeof Card>; ``` **Why good:** `name` property enables component-specific themes (`dark_Card`), spread variant `...size` maps all size tokens automatically, `as const` preserves literal types for variant inference, `GetProps` keeps prop type in sync with the component ```tsx // BAD: raw values bypass token system export const Card = styled(View, { padding: 16, // raw pixel -- not theme-aware borderRadius: 8, // should be $4 or a radius token backgroundColor: "#fff", // hardcoded -- breaks dark mode }); ``` **Why bad:** raw values bypass token resolution and theme switching, hardcoded colors break in dark mode, no connection to design system See [examples/core.md](examples/core.md) for full variant patterns including boolean, functional, and nested `styled(styled())`. --- ### Pattern 2: Token System and Theme Hierarchy Tokens define the design scale. Themes override token values within React subtrees. Components reference `$`-prefixed keys that resolve to the active theme, falling back to tokens. ```tsx // Token reference in JSX <YStack padding="$4" gap="$2" backgroundColor="$background"> <SizableText size="$5" color="$color"> Themed text </SizableText> </YStack> // Theme nesting -- inner Theme applies as "dark_green" <Theme name="dark"> <Card> <Theme name="green"> <Card>{/* uses dark_green theme */}</Card> </Theme> </Card> </Theme> ``` **Why good:** `$background` resolves from active theme (light or dark), nested Theme composes name automatically (`dark` + `green` = `dark_green`), missing keys in sub-theme fall back to parent theme then to tokens See [examples/core.md](examples/core.md) for `createTokens`, `createTheme`, theme definition patterns, and `useTheme` hook usage. --- ### Pattern 3: Compiler Optimization The compiler flattens styled components to native primitives (`div` on web, `View` on native) and extracts atomic CSS. This only works when styles are **deterministic at build time**. ```tsx // GOOD: compiler can flatten -- all values are static tokens <Card size="$4" elevated /> // GOOD: media query props are compiler-optimized <YStack padding="$2" $gtMd={{ padding: "$4" }} /> // BAD: dynamic ternary prevents flattening <YStack padding={isLarge ? "$4" : "$2"} /> // BAD: function render prop deoptimizes <Card render={(props) => <CustomThing {...props} />} /> ``` **Key rule:** Static token values, spread variants, and media query props are compiler-friendly. JavaScript expressions, ternaries on runtime values, and function render props force runtime evaluation. See [examples/core.md](examples/core.md) for what the compiler can/cannot optimize and how to structure code for maximum flattening. --- ### Pattern 4: Responsive Styles with Media Queries Use `$`-prefixed media query props for responsive styles. The compiler extracts these to CSS `@media` rules on web, eliminating runtime overhead. ```tsx <XStack flexDirection="column" padding="$2" $gtSm={{ flexDirection: "row", padding: "$4" }} $gtMd={{ gap: "$4" }} > <Card flex={1} /> <Card flex={1} /> </XStack> ``` **Why good:** mobile-first base styles, media props override progressively, compiler extracts to CSS @media rules on web (zero JS runtime) See [examples/responsive-animations.md](examples/responsive-animations.md) for `useMedia` hook, config breakpoints, and height-based queries. --- ### Pattern 5: Animations with transition Prop The `transition` prop references a named animation from your config. Combine with `enterStyle`, `exitStyle`, `hoverStyle`, and `pressStyle` for declarative animations. ```tsx <Card transition="bouncy" enterStyle={{ opacity: 0, scale: 0.9, y: -10 }} hoverStyle={{ scale: 1.02 }} pressStyle={{ scale: 0.98 }} opacity={1} scale={1} y={0} /> ``` **Why good:** declarative animation states, driver-agnostic (CSS on web, Reanimated on native), SSR-safe enterStyle **Critical:** always keep `transition` in JSX -- pass `null` to disable, never conditionally omit the prop (causes expensive hook teardown). See [examples/responsive-animations.md](examples/responsive-animations.md) for driver configuration, `AnimatePresence`, and per-property `animateOnly`. --- ### Pattern 6: Sheet and Dialog with Adapt Use `Adapt` to render Dialog as a Sheet on touch devices at smaller breakpoints. This avoids manual `Platform.OS` branching and responds to viewport changes. ```tsx <Dialog modal> <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 /> <Dialog.Content> <Dialog.Title>Title</Dialog.Title> <Dialog.Description>Description</Dialog.Description> <Dialog.Close asChild> <Button>Close</Button> </Dialog.Close> </Dialog.Content> </Dialog.Portal> </Dialog> ``` **Why good:** single component tree handles both desktop dialog and mobile sheet, `Adapt.Contents` injects Dialog.Content into Sheet.Frame, breakpoint-driven not platform-driven See [examples/overlays.md](examples/overlays.md) for controlled Sheet, snap points, ScrollView inside Sheet, and Dialog state preservation. </patterns> --- <decision_framework> ## Decision Framework ### When to Use styled() vs Inline Props ``` Is this a reusable component with variants or a semantic name? ├─ YES → styled() with name, variants, defaultVariants └─ NO → Is this one-off layout? ├─ YES → Inline props on XStack/YStack (<YStack padding="$4">) └─ NO → styled() if you want component themes or compiler naming ``` ### Animation Driver Selection ``` Platform target? ├─ Web only → @tamagui/animations-css (smallest bundle, CSS transitions) ├─ Native only → @tamagui/animations-reanimated (worklet-based, spring physics) ├─ Universal → Configure per-platform in createTamagui │ ├─ Web: CSS or Motion driver │ └─ Native: Reanimated or React Native driver └─ Simple transitions? → @tamagui/animations-react-native (no extra dependency) ``` ### Overlay Component Choice ``` Need bottom sheet on mobile? ├─ YES → Is there also a desktop version? │ ├─ YES → Dialog + Adapt + Sheet (single component tree) │ └─ NO → Sheet standalone └─ NO → Dialog (portal-based overlay) ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Using raw pixel values (`padding: 16`) instead of tokens (`padding: "$4"`) -- bypasses theme system, breaks consistency across platforms - Conditionally removing the `transition` prop from JSX -- causes expensive spring hook teardown/setup; pass `null` to disable instead - Missing `as const` on variant definition objects -- TypeScript infers `string` instead of literal union types, losing autocomplete and type safety - Using `Platform.OS` branching for Dialog vs Sheet -- use `Adapt` instead, which responds to breakpoints and is compiler-optimized - Hardcoded color strings (`"#fff"`, `"#000"`) in styled components -- breaks dark/light theme switching; use `$background`, `$color` **Medium Priority Issues:** - Not setting `name` on styled components that need component-level themes -- without `name`, Tamagui cannot look up component-specific theme variants like `dark_Card` - Using JavaScript ternaries for responsive styles instead of media query props -- prevents compiler CSS extraction, adds runtime cost - Nesting `styled(styled())` without `.styleable()` when wrapping with a functional component -- variant merging breaks silently - Importing from `tamagui` instead of `@tamagui/core` when you only need the core -- pulls in the entire UI kit unnecessarily **Gotchas & Edge Cases:** - **Theme nesting is name-based:** `<Theme name="dark"><Theme name="green">` resolves to `dark_green`, not just `green`. The sub-theme must be defined as `dark_green` in your config. - **`Object.groupBy` on `useMedia()` result fails:** The proxied object from `useMedia` is not iterable -- use direct key access (`media.sm`) only. - **`Dialog.Sheet` does not preserve state** when transitioning between Sheet and Portal modes. Lift state above the Dialog if persistence is needed. - **Spread variants (`...size`) only match top-level token categories** -- custom nested token groups require functional variants instead. - **`elevation` prop** generates both shadow props (iOS) and elevation (Android) on native, but translates to `box-shadow` on web. Differences in shadow appearance across platforms are expected. - **Config v5 changed flex defaults:** With `styleCompat: 'react-native'`, `flex` uses `flexBasis: 0` (not `auto`). Without it, web defaults apply. - **The Moti animation driver is deprecated** -- switch to `@tamagui/animations-reanimated` (same API, fewer dependencies). - **String-to-boolean coercion in config** -- if parsing env vars for config flags, the string `"false"` is truthy in JavaScript. Use explicit comparison (`val === "true"`) not coercion. </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST use `$`-prefixed token values in style props (`$4`, `$color.blue`) -- raw pixel values bypass the token system and break theme consistency)** **(You MUST keep the `transition` prop present in JSX when using animations -- conditionally removing it causes expensive hook teardown; pass `null` to disable instead)** **(You MUST add `as const` to variant definition objects -- without it TypeScript cannot infer variant prop types correctly)** **(You MUST use `Adapt` for responsive Dialog-to-Sheet behavior -- manual Platform.OS branching breaks compiler optimization and misses breakpoint changes)** **Failure to follow these rules will break theme consistency, cause animation performance issues, lose type safety on variants, and produce non-adaptive overlays.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.