Skip to content

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

FixDevs ·

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).

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.

For related mobile UI issues, see Fix: NativeWind Not Working and Fix: Expo Not Working.

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