Sharing a codebase between web and mobile with Tamagui

I've been working on a recent client project that needed both a web app and a mobile app. We really didn't want to maintain two completely separate UI codebases—doubling the work for every button, form and screen sounded miserable. So we started looking at Tamagui which lets you write components once and ship them to both Next.js (web) and React Native (mobile).

The setup

The trick is a monorepo. I used yarn workspaces but Turborepo or pnpm workspaces work just as well. The structure looks something like:

apps/
  next/        // Next.js web app
  expo/        // mobile app
packages/
  ui/          // shared components
  config/      // tamagui.config.ts + tokens

Both apps depend on packages/ui and packages/config. That's where the magic happens—any component you write in packages/ui runs on web and native.

The config

Tamagui needs a single source of truth for tokens, themes and fonts. This lives in packages/config/tamagui.config.ts:

import { createTamagui } from "tamagui";
import { config } from "@tamagui/config/v3";

export const tamaguiConfig = createTamagui(config);

export default tamaguiConfig;

You then wrap both apps in a TamaguiProvider and pass this same config. Both platforms now share the same design system.

Writing a shared component

You import primitives from tamagui (not from react-native or a web library) and they just work on both platforms:

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

export function WelcomeCard({ name }: { name: string }) {
	return (
		<YStack padding="$4" gap="$2" backgroundColor="$background">
			<Text fontSize="$6" fontWeight="700">
				Hey {name}
			</Text>
			<Button theme="active">Get started</Button>
		</YStack>
	);
}

YStack is a vertical flex container, $4 is a token from the config, and the same JSX renders as a <div> on web and a View on native.

What about the compiler?

Tamagui ships an optional compiler that extracts your styles into CSS at build time. For web, this means the styles aren't computed in JS at runtime—you get static CSS, which is properly fast. For native, it flattens style props into a single StyleSheet.

You enable it in next.config.js with @tamagui/next-plugin and in the Expo app via the babel plugin. It is a bit fiddly to set up the first time but the docs walk you through it.

Things to watch out for

  • Platform-specific files still exist. If you need something only on web, use Component.web.tsx. Only on native, use Component.native.tsx.
  • Not every web HTML element has a Tamagui equivalent. For things like <form> you'll need to wrap them yourself.
  • The bundle size on web is bigger than a purely web-first library like Radix. The trade-off is one codebase instead of two.
  • Of course there will be some platform-specific quirks. You may need to tweak styles or logic for mobile vs desktop.

For a project where you need both platforms without writing everything twice, it's been a really nice setup.