Skip to content

Fix: Tamagui Not Working — Styles Not Applying, Compiler Errors, or Web/Native Mismatch

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix Tamagui UI kit issues — setup with Expo, theme tokens, styled components, animations, responsive props, media queries, and cross-platform rendering.

The Problem

Tamagui components render without styles:

import { Button, Text, YStack } from 'tamagui';

function App() {
  return (
    <YStack padding="$4" backgroundColor="$background">
      <Text fontSize="$6" color="$color">Hello</Text>
      <Button>Click me</Button>
    </YStack>
  );
}
// Components render but with no visual styling

Or the compiler throws during build:

Error: Tamagui: Expected theme to be defined

Or styles work on web but not on native (or vice versa):

Button looks correct in web browser but has wrong colors on iOS

Why This Happens

Tamagui is a universal UI kit that works on React Native and web. It uses a compile-time optimizer and token-based theming:

  • TamaguiProvider with a config is required — all Tamagui components read theme tokens and settings from a provider. Without it, $4, $background, $color resolve to nothing.
  • The tamagui config must define tokens and themes — tokens are design values (spacing, colors, fonts). Themes map semantic names to token values. Missing definitions cause “Expected theme” errors.
  • The compiler plugin optimizes styles at build time — Tamagui extracts static styles into CSS (web) or StyleSheet (native) during compilation. Without the plugin, styles work but aren’t optimized, and some features may break.
  • Web and native have different rendering paths — on web, Tamagui outputs CSS. On native, it outputs StyleSheet objects. Platform-specific bugs usually stem from features that exist on one platform but not the other (e.g., CSS hover states don’t exist on native).

The biggest mental shift versus a runtime-only styling library like styled-components is that Tamagui does real work at build time. Its babel/swc plugin walks JSX, evaluates style props it can flatten, and rewrites the tree into a static CSS class (web) or a hoisted StyleSheet.create call (native). When the plugin is missing or partially configured, components still render — they just fall back to the slow runtime path. That mostly looks like a perf regression, but some advanced features (variants with dynamic enterStyle, certain media-prop combinations) only work when the compiler is active. So “Tamagui not working” can mean three completely different failure modes: provider missing (no styles at all), compiler missing (slow + partial features), or config mismatch (wrong tokens).

The cross-platform split is the second big trap. Tamagui treats web and React Native as first-class targets, but they use different style engines under the hood. On web, the compiler emits atomic CSS classes that get appended to the document. On native, it emits StyleSheet objects that React Native’s Yoga layout engine consumes. Properties that only exist in CSS (cursor, backdropFilter, complex boxShadow strings) silently no-op on native. Properties that only exist in React Native (elevation on Android, shadowOpacity on iOS) silently no-op on web. Tamagui’s tokens paper over a lot of this, but custom styles bypass that translation layer.

Tamagui Version History

The version timeline explains why so many tutorials give conflicting instructions:

  • Tamagui 1.0 (May 2023) — the official 1.0 release. Locked in createTamagui as the config entry point, established the @tamagui/config package for default tokens, and stabilized the compiler API. Anything from before May 2023 used a different config shape and is no longer valid.
  • Tamagui 1.10 — 1.30 (mid 2023) — added the v2 config preset (@tamagui/config/v2), the Theme component for nested theme switching, and the animation prop driven by react-native-reanimated. The static optimizer became significantly more aggressive at flattening.
  • Tamagui 1.50+ (late 2023 / early 2024) — shipped @tamagui/config/v3, which is the current recommended default. v3 includes a redesigned color palette, refined spacing scale, and better dark mode tokens. Existing v2 configs keep working, but new projects should start on v3.
  • Tamagui 1.70+ (2024) — added improved Next.js App Router support, better SSR handling for CSS extraction, and tighter Expo Router integration. The compiler plugin now ships per-bundler entries (@tamagui/babel-plugin, @tamagui/vite-plugin, @tamagui/metro-plugin).

The practical implication: when you copy a createTamagui snippet from a blog post, check whether it imports from @tamagui/config, @tamagui/config/v2, or @tamagui/config/v3. Mixing v2 token names with a v3 base config produces the “Expected theme” error even when both packages are installed. Pin all @tamagui/* packages to the same minor version — they share internal types and break across mismatched minors.

One more historical note: the bundler integration story has changed over time. Early Tamagui projects used the babel plugin everywhere — Metro on native, Babel-via-Vite on web. Modern Tamagui (1.50+) ships dedicated @tamagui/vite-plugin and @tamagui/metro-plugin packages that wrap the compiler with bundler-specific glue. If your project predates these, migrate to the dedicated plugins; the standalone babel plugin still works but loses access to newer optimizations like the inline-theme transform and the dead-code elimination pass for unused tokens.

Compared to alternatives in the React Native UI space, Tamagui’s distinguishing feature is the compile-time optimizer. NativeWind, Restyle, and Dripsy all do runtime style resolution — they’re easier to adopt but pay a per-render cost. Tamagui’s compiler trades a more complex setup (provider, config, plugin, peer-dep alignment) for near-static-CSS performance and fully shared styles between web and native. When the setup works, it’s hard to beat. When it doesn’t, the failure modes (silent compiler fallback, mismatched config versions, provider missing) are subtler than what you’d see with a runtime-only library. That trade-off is the lens to keep in mind when debugging: most “not working” issues are actually compiler or config issues, not styling logic bugs.

Fix 1: Setup with Expo

npx create-tamagui@latest --template expo-router
# Or add to existing Expo project:
npm install tamagui @tamagui/config
npx expo install react-native-reanimated
// tamagui.config.ts
import { createTamagui } from 'tamagui';
import { config } from '@tamagui/config/v3';

// Use the default config (includes tokens, themes, fonts, etc.)
export const tamaguiConfig = createTamagui(config);

// Or customize:
export const tamaguiConfig = createTamagui({
  ...config,
  tokens: {
    ...config.tokens,
    color: {
      ...config.tokens.color,
      primary: '#3b82f6',
      secondary: '#64748b',
    },
  },
  themes: {
    ...config.themes,
    // Custom themes extend or override defaults
  },
});

// Type declaration
export type AppConfig = typeof tamaguiConfig;

declare module 'tamagui' {
  interface TamaguiCustomConfig extends AppConfig {}
}
// app/_layout.tsx — wrap with TamaguiProvider
import { TamaguiProvider } from 'tamagui';
import { tamaguiConfig } from '../tamagui.config';

export default function RootLayout() {
  return (
    <TamaguiProvider config={tamaguiConfig} defaultTheme="light">
      <Stack />
    </TamaguiProvider>
  );
}
// metro.config.js — add Tamagui transform
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

// Add tamagui file extensions
config.resolver.sourceExts.push('mjs');

module.exports = config;

Fix 2: Core Components

import {
  YStack, XStack, Text, Button, Input, Card, H1, H2, Paragraph,
  Separator, Avatar, Sheet, Switch, Label, Checkbox, RadioGroup,
  ScrollView, Image, Spinner,
} from 'tamagui';

function ProfileScreen() {
  return (
    <ScrollView flex={1} backgroundColor="$background">
      {/* Vertical stack */}
      <YStack padding="$4" gap="$4">
        {/* Header */}
        <XStack alignItems="center" gap="$3">
          <Avatar circular size="$6">
            <Avatar.Image src="https://example.com/avatar.jpg" />
            <Avatar.Fallback backgroundColor="$blue5" />
          </Avatar>
          <YStack>
            <H2>Alice Johnson</H2>
            <Paragraph theme="alt2">[email protected]</Paragraph>
          </YStack>
        </XStack>

        <Separator />

        {/* Card */}
        <Card elevate bordered padding="$4">
          <Card.Header>
            <H2>Settings</H2>
          </Card.Header>

          {/* Toggle */}
          <XStack alignItems="center" justifyContent="space-between" paddingVertical="$2">
            <Label htmlFor="notifications">Notifications</Label>
            <Switch id="notifications" size="$3">
              <Switch.Thumb animation="bouncy" />
            </Switch>
          </XStack>

          <XStack alignItems="center" justifyContent="space-between" paddingVertical="$2">
            <Label htmlFor="darkMode">Dark Mode</Label>
            <Switch id="darkMode" size="$3">
              <Switch.Thumb animation="bouncy" />
            </Switch>
          </XStack>
        </Card>

        {/* Input */}
        <YStack gap="$2">
          <Label htmlFor="bio">Bio</Label>
          <Input id="bio" placeholder="Tell us about yourself..." />
        </YStack>

        {/* Buttons */}
        <XStack gap="$3">
          <Button flex={1} theme="active">Save</Button>
          <Button flex={1} variant="outlined">Cancel</Button>
        </XStack>
      </YStack>
    </ScrollView>
  );
}

Fix 3: Responsive and Adaptive Props

import { YStack, Text, XStack, useMedia } from 'tamagui';

// Media query props — change based on screen size
function ResponsiveLayout() {
  return (
    <YStack
      padding="$3"
      $gtSm={{ padding: '$4' }}     // > 660px
      $gtMd={{ padding: '$6' }}     // > 800px
      $gtLg={{ padding: '$8' }}     // > 1020px
    >
      <XStack
        flexDirection="column"
        $gtSm={{ flexDirection: 'row' }}
        gap="$4"
      >
        <YStack flex={1}>
          <Text
            fontSize="$5"
            $gtMd={{ fontSize: '$8' }}
          >
            Responsive heading
          </Text>
        </YStack>
        <YStack
          width="100%"
          $gtSm={{ width: '50%' }}
          $gtMd={{ width: '33%' }}
        >
          <Text>Sidebar content</Text>
        </YStack>
      </XStack>
    </YStack>
  );
}

// useMedia hook for conditional rendering
function AdaptiveContent() {
  const media = useMedia();

  return (
    <YStack>
      {media.gtMd ? (
        <DesktopLayout />
      ) : (
        <MobileLayout />
      )}
      <Text>{media.sm ? 'Small screen' : 'Large screen'}</Text>
    </YStack>
  );
}

Fix 4: Themes and Dark Mode

import { YStack, Text, Button, useTheme, Theme } from 'tamagui';
import { useColorScheme } from 'react-native';

// Theme switching
function ThemedApp() {
  const colorScheme = useColorScheme();

  return (
    <TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme || 'light'}>
      <AppContent />
    </TamaguiProvider>
  );
}

// Nested theme — override for a section
function DarkSection() {
  return (
    <Theme name="dark">
      <YStack backgroundColor="$background" padding="$4">
        <Text color="$color">This section is always dark</Text>
      </YStack>
    </Theme>
  );
}

// Access theme values programmatically
function ThemedComponent() {
  const theme = useTheme();

  return (
    <YStack>
      <Text>Primary color: {theme.color.val}</Text>
      <Text>Background: {theme.background.val}</Text>
    </YStack>
  );
}

// Sub-themes (color variations)
<Theme name="blue">
  <Button>Blue themed button</Button>
</Theme>

<Theme name="red">
  <Button theme="active">Red active button</Button>
</Theme>

Fix 5: Animations

import { YStack, Text, Button, AnimatePresence } from 'tamagui';
import { useState } from 'react';

function AnimatedCard() {
  const [show, setShow] = useState(true);

  return (
    <YStack>
      <Button onPress={() => setShow(!show)}>Toggle</Button>

      <AnimatePresence>
        {show && (
          <YStack
            animation="bouncy"
            enterStyle={{ opacity: 0, scale: 0.9, y: -10 }}
            exitStyle={{ opacity: 0, scale: 0.9, y: -10 }}
            opacity={1}
            scale={1}
            y={0}
            backgroundColor="$background"
            padding="$4"
            borderRadius="$4"
            elevation="$2"
          >
            <Text>Animated content</Text>
          </YStack>
        )}
      </AnimatePresence>
    </YStack>
  );
}

// Hover and press animations (web + native)
function InteractiveButton() {
  return (
    <Button
      animation="quick"
      hoverStyle={{ scale: 1.05, backgroundColor: '$blue6' }}
      pressStyle={{ scale: 0.95, opacity: 0.8 }}
      focusStyle={{ borderColor: '$blue8' }}
    >
      Interactive Button
    </Button>
  );
}

Fix 6: Custom Styled Components

import { styled, YStack, Text, GetProps } from 'tamagui';

// Create custom styled components
const Card = styled(YStack, {
  backgroundColor: '$background',
  borderRadius: '$4',
  padding: '$4',
  borderWidth: 1,
  borderColor: '$borderColor',

  // Variants
  variants: {
    elevated: {
      true: {
        elevation: '$2',
        shadowColor: '$shadowColor',
      },
    },
    size: {
      sm: { padding: '$2' },
      md: { padding: '$4' },
      lg: { padding: '$6' },
    },
  } as const,

  // Default variants
  defaultVariants: {
    size: 'md',
  },
});

// Extract props type
type CardProps = GetProps<typeof Card>;

// Usage
<Card elevated size="lg">
  <Text>Custom card component</Text>
</Card>

// Extend further
const FeatureCard = styled(Card, {
  borderLeftWidth: 4,
  borderLeftColor: '$blue8',

  variants: {
    type: {
      info: { borderLeftColor: '$blue8' },
      success: { borderLeftColor: '$green8' },
      warning: { borderLeftColor: '$yellow8' },
      error: { borderLeftColor: '$red8' },
    },
  } as const,
});

<FeatureCard type="success" elevated>
  <Text>Feature card</Text>
</FeatureCard>

Still Not Working?

Components have no stylesTamaguiProvider with a valid config must wrap your app. Without it, token references ($4, $background) resolve to empty values. Import the config from @tamagui/config/v3 for defaults.

“Expected theme to be defined” — the theme referenced by a component or the defaultTheme prop doesn’t exist in the config. Check that light and dark themes are defined. If using custom theme names, ensure they’re in tamaguiConfig.themes.

Styles work on web but not native — some CSS properties don’t exist on React Native (e.g., cursor, userSelect, CSS gradients). Tamagui handles many cross-platform differences, but custom styles may need platform-specific values using $platform-native or $platform-web prefixes.

Animations don’t play — install react-native-reanimated and add the babel plugin. Tamagui uses Reanimated for native animations. Without it, animation props are silently ignored on native.

Compiler optimization warnings in production — the build log says “Tamagui: could not flatten component.” That means the compiler hit a prop it can’t statically analyze (a function call, a destructured variable, a runtime condition) and fell back to runtime rendering. The component still works, just slower. To force flattening, lift dynamic values out of style props or precompute them.

Mixed @tamagui/config versions — installing [email protected] with @tamagui/[email protected] leaves tokens stale. Symptoms include themes that look “almost right” but with subtly wrong spacing or colors. Audit package.json for any @tamagui/* packages and align them to the same minor version, then delete node_modules and reinstall.

Hot reload stops applying style changes — Metro and Vite cache the compiler output aggressively. If a config change isn’t picked up, clear the bundler cache (expo start --clear for Expo, restart the Vite dev server for web). For persistent caches, also delete .tamagui if present at the project root.

For related mobile UI issues, see Fix: NativeWind Not Working and Fix: Expo Not Working. If animations specifically aren’t running, see Fix: React Native Reanimated Not Working. For Metro bundler problems blocking Tamagui’s compiler, see Fix: React Native Metro Bundler Failed.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles