Tamagui Compiler

Adding the compiler to your apps

The Tamagui Compiler significantly improves performance of both web and native applications through partial analysis and view flattening.

See the Benchmarks or read the in-depth background. Note that Tamagui features work at compile-time and runtime, so installing the compiler is optional, and in fact we recommend only setting it up once you’re ready for production.

The compiler uses Babel to analyze JSX and styled functions, then attempts to statically analyze and optimize them down to platform-native primitives. The end result is less abstraction, such as a div on web or a plain React Native View on native:



Use inline style props or styled():

Input
import { View, Text, styled } from '@tamagui/core'
import { Heading } from './Heading'
const App = (props) => (
<View px="10px gtSm:30px" width={550}>
<Heading size={props.big ? 'large' : 'small'}>Lorem ipsum.</Heading>
</View>
)
const Heading = styled(Text, {
render: 'h1',
color: 'green',
backgroundColor: 'background',
variants: {
size: {
large: {
fontSize: 22,
},
small: {
fontSize: 16,
},
},
},
})

Get back perfectly optimized DOM on web (or View/Text on native).

Output
export const App = props => <div className={_cn}>
<h1 className={_cn2 + (_cn3 + (props.big ? _cn4 : _cn5))}>
Lorem ipsum.
</h1>
</div>
const _cn5 = " _fos-16px"
const _cn4 = " _fos-22px"
const _cn3 = " _bg-180kg62 _col-b5vn3b _mt-0px _mr-0px _mb-0px _ml-0px _ww-break-word _bxs-border-box _ff-System _dsp-inline "
const _cn2 = " font_System"
const _cn = " _fd-column _miw-0px _mih-0px _pos-relative _bxs-border-box _fb-auto _dsp-flex _fs-0 _ai-stretch _w-550px _pr-1aj14ca _pl-1aj14ca _pr-_gtSm_lrpixp _pl-_gtSm_lrpixp"

The compiler generates built versions of your components and config into a .tamagui directory. You’ll want to add that directory to your .gitignore.

Configuration with tamagui.build.ts

We recommend creating a tamagui.build.ts file in your project root as the single source of truth for your compiler configuration. All bundler plugins and the CLI automatically read from this file, so you only need to define your options once.

This file lets the Tamagui CLI read your config and perform operations like generating CSS, pre-compiling components, and verifying optimizations, while also sharing that same configuration with whichever bundler plugin you use. Without it, you’d need to duplicate options across your metro, babel, vite, or webpack configs.

tamagui.build.ts

import type { TamaguiBuildOptions } from 'tamagui'
export default {
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: './public/tamagui.generated.css',
// optional:
importsWhitelist: ['constants.js', 'colors.js'],
disableExtraction: process.env.NODE_ENV === 'development',
} satisfies TamaguiBuildOptions

With this file in place, your bundler plugins can be configured with no options at all, because they pick up everything from tamagui.build.ts:

// vite: tamaguiPlugin()
// webpack: new TamaguiPlugin()
// metro: withTamagui(config)
// all read from tamagui.build.ts automatically

You can still pass options directly to a plugin, and they’ll be merged with (and override) the options from tamagui.build.ts.

Experimental native theme fast path

V3 beta includes an opt-in native theme engine for React Native 0.82 and newer. It can update eligible Fabric views through a ShadowTree commit without re-rendering each view in React. The API and compiler output are experimental.

Install the native engine and Nitro runtime:

bun add @tamagui/native-registry react-native-nitro-modules

Enable its compiler output in tamagui.build.ts:

import type { TamaguiBuildOptions } from 'tamagui'
export default {
config: './tamagui.config.ts',
components: ['tamagui'],
experimental: {
nativeFastPath: true,
},
} satisfies TamaguiBuildOptions

Install the engine before your themed application tree mounts:

import * as nativeRegistry from '@tamagui/native-registry'
import { setNativeStyleEngine } from '@tamagui/core'
if (nativeRegistry.isAvailable()) {
setNativeStyleEngine(nativeRegistry)
}

Rebuild the iOS and Android applications after adding the packages. The flag defaults to false. When it is off, native compiler output is unchanged. When the compiler flag is on but the engine is unavailable, components use the existing theme hook and React render path. Web output is unchanged.

The registry keeps the existing DynamicColorIOS optimization in place. It does not replace that path.

Install

The recommended install is one package. @tamagui/cli carries the CLI and the bundler integrations together, each behind a subpath:

yarn add -D @tamagui/cli
ImportGives you
@tamagui/cli/vitethe same exports as @tamagui/vite-plugin
@tamagui/cli/metrothe same exports as @tamagui/metro-plugin
Import
@tamagui/cli/vite
Gives you
the same exports as @tamagui/vite-plugin
Import
@tamagui/cli/metro
Gives you
the same exports as @tamagui/metro-plugin

One dependency means one version to keep in sync, so the CLI and your bundler plugin can’t drift apart on an upgrade.

You can still install the standalone plugin instead, and if your project only ever uses one bundler that is the smaller install. Both packages stay published and the exports are identical, so the setup below is the same either way.

There are plugins for a variety of bundlers, or you can use the @tamagui/cli to compile in-place:

Webpack

yarn add tamagui-loader

We have a full example of a plain Webpack or Vite setup in the simple starter accessible through npm create tamagui@latest, which shows a complete configuration with more detail.

Add tamagui-loader and set up your webpack.config.js.

You can set it up more manually like so:

const { shouldExclude } = require('tamagui-loader')
const tamaguiOptions = {
config: './tamagui.config.ts',
components: ['tamagui'],
importsWhitelist: ['constants.js', 'colors.js'],
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
}
module.exports = {
resolve: {
alias: {
// Resolve react-native to react-native-web
'react-native$': require.resolve('react-native-web'),
// optional, for lighter svg icons on web
'react-native-svg': require.resolve('@tamagui/react-native-svg'),
},
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
// you'll likely want to adjust this helper function,
// but it serves as a decent start that you can copy/paste from
exclude: (path) => shouldExclude(path, __dirname, tamaguiOptions),
use: [
// optionally thread-loader for significantly faster compile!
'thread-loader',
// works nicely alongside esbuild
{
loader: 'esbuild-loader',
},
{
loader: 'tamagui-loader',
options: tamaguiOptions,
},
],
},
],
},
}

Or you can use the TamaguiPlugin which automates some of this setup for you. If you have a tamagui.build.ts, you can pass no options:

const { TamaguiPlugin } = require('tamagui-loader')
module.exports = {
plugins: [
// reads from tamagui.build.ts automatically
new TamaguiPlugin(),
],
}

Or pass options inline to override:

const { TamaguiPlugin } = require('tamagui-loader')
module.exports = {
plugins: [
new TamaguiPlugin({
config: './tamagui.config.ts',
components: ['tamagui'],
importsWhitelist: ['constants.js', 'colors.js'],
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
}),
],
}

Some notes on the options:

  • importsWhitelist: Tamagui takes a conservative approach to partial evaluation, this field whitelists (matching against both .ts and .js) files to allow files that import them to read and use their values during compilation. Typically colors and constants files.
  • disableExtraction: Useful for faster developer iteration as your design system hot reloads more reliably.

Vite

See the Vite guide for more complete setup.

The Vite plugin is ESM-only. Your project must have "type": "module" in its package.json (or use .mjs/.mts config files).

Update your vite.config.ts. If you have a tamagui.build.ts, no options are needed:

import { tamaguiPlugin } from '@tamagui/cli/vite'
export default defineConfig({
plugins: [
// reads from tamagui.build.ts automatically
tamaguiPlugin(),
],
})

If you installed @tamagui/vite-plugin on its own, import from there instead.

Or pass options inline:

import { tamaguiPlugin } from '@tamagui/cli/vite'
export default defineConfig({
plugins: [
tamaguiPlugin({
config: 'src/tamagui.config.ts',
components: ['tamagui'],
disableExtraction: true,
}),
],
})

Next.js

See the guide for more complete setup.

Next.js with Turbopack (the default) works best with the CLI approach: create a tamagui.build.ts and use tamagui build to optimize production builds. No bundler plugin needed.

For older Webpack-based Next.js setups, add @tamagui/next-plugin and configure your next.config.js:

const { withTamagui } = require('@tamagui/next-plugin')
module.exports = function (name, { defaultConfig }) {
const tamaguiPlugin = withTamagui({
// reads from tamagui.build.ts automatically, or pass inline:
config: './tamagui.config.ts',
components: ['tamagui'],
disableExtraction: process.env.NODE_ENV === 'development',
excludeReactNativeWebExports: ['Switch', 'ProgressBar', 'Picker'],
})
return {
...defaultConfig,
...tamaguiPlugin(defaultConfig),
}
}

Note: If running into issues, the environment variable IGNORE_TS_CONFIG_PATHS to “true” can fix issues with Tamagui being resolved incorrectly.

See the Next.js Guide for more details on setting up your app.

Metro

The Metro integration runs the same shared compiler used by the other bundler adapters after your configured Babel transformer.

Wrap your existing Metro config. With a tamagui.build.ts, you can omit the inline options:

const { getDefaultConfig } = require('expo/metro-config')
const { withTamagui } = require('@tamagui/cli/metro')
module.exports = withTamagui(getDefaultConfig(__dirname))

If you installed @tamagui/metro-plugin on its own, require from there instead.

Or pass options inline:

const { getDefaultConfig } = require('expo/metro-config')
const { withTamagui } = require('@tamagui/cli/metro')
module.exports = withTamagui(getDefaultConfig(__dirname), {
components: ['tamagui'],
config: './tamagui.config.ts',
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
})

Expo

Check out the Expo guide for more information on setting up Expo. It uses the same Metro integration shown above.

CLI-Based In-Place Compilation

For bundlers that don’t have a Tamagui plugin yet (like Turbopack), or if you prefer a simple setup, you can use @tamagui/cli to pre-compile your components in-place before your build step.

This approach is meant for production builds only and should run in your deployment pipeline, not during development. It rewrites files in place which will mess up your working directory, but makes it highly compatible with any bundler or tool. The downside is you don’t get the helpful development compatibility parts of the plugins, plus dev-mode debugging and data- attributes.

For complete CLI documentation including all available commands, see the CLI Guide.

Setup

  1. Install:
yarn add -D @tamagui/cli
  1. Create a tamagui.build.ts if you haven’t already (see above).

  2. Add a build script to your package.json:

{ "scripts": { "build": "tamagui build ./src -- next build" } }

Usage

Terminal

# Build all components in a directory (web + native by default) npx tamagui build ./src # Build for web only npx tamagui build --target web ./src # Build for native only npx tamagui build --target native ./src # Build a specific file npx tamagui build ./src/components/MyComponent.tsx # Include/exclude patterns npx tamagui build --include "components/**" --exclude "**/*.test.tsx" ./src # Output to a separate directory (source files unchanged) npx tamagui build --output ./dist ./src # Create platform-specific files next to source files (.web.tsx or .native.tsx) npx tamagui build --target native --output-around ./src # Preview changes without writing files npx tamagui build --dry-run ./src # Verify minimum optimizations (useful in CI) npx tamagui build --target web --expect-optimizations 10 ./src

CI Verification with –expect-optimizations

The --expect-optimizations flag ensures your build is actually optimizing components. This is useful in CI to catch configuration issues:

{ "scripts": { "build": "tamagui build --target web --expect-optimizations 10 ./src -- next build" } }

If the compiler produces fewer than the expected number of optimizations, the build will fail with an error message showing the actual count. This helps catch:

  • Misconfigured components array
  • Wrong source paths
  • Configuration files not being found

Platform-Specific File Handling

The CLI automatically handles platform-specific files (.web.tsx, .native.tsx, .ios.tsx, .android.tsx):

  • Files with .web.tsx extensions are optimized for web only
  • Files with .native.tsx, .ios.tsx, or .android.tsx extensions are optimized for native only
  • Base files (.tsx) without platform-specific versions are optimized for all platforms
  • If both .web.tsx and .native.tsx exist, the base .tsx file is skipped

Package.json Exports Support

The CLI supports package.json exports for path-specific imports. For example:

{ "exports": { ".": "./src/index.tsx", "./components/Button": "./src/Button.tsx" } }

Both import styles work:

import { Button } from '@my/ui'
import { Button } from '@my/ui/components/Button'

Integration Examples

This works with any build tool: run tamagui build before your build command. Here are some examples:

Next.js with Turbopack (Turbopack doesn’t support plugins yet):

{ "scripts": { "dev": "next dev --turbopack", "build": "tamagui build --target web ./src -- next build" } }

Vite, Remix, or any other bundler:

{ "scripts": { "build": "tamagui build --target web ./src -- vite build" } }

React Native / Expo:

{ "scripts": { "build:ios": "tamagui build --target native ./src -- eas build --platform ios", "build:android": "tamagui build --target native ./src -- eas build --platform android" } }

Using –output (no file restoration needed):

If you prefer to output optimized files to a separate directory instead of modifying source files in-place, use --output:

{ "scripts": { "build": "tamagui build --target web --output ./dist ./src && next build" } }

With --output, your source files are never modified. The optimized files are written to the output directory with their directory structure preserved.

Using –output-around (platform-specific files):

The --output-around flag creates optimized platform-specific files (.web.tsx or .native.tsx) next to your source files instead of modifying them. Bundlers automatically pick up these files via platform-specific resolution:

{ "scripts": { "prebuild:native": "tamagui build --target native --output-around ./src", "prebuild:web": "tamagui build --target web --output-around ./src" } }

This transforms Button.tsx → creates Button.native.tsx or Button.web.tsx alongside it. Metro/Expo uses .native.tsx on native, and web bundlers use .web.tsx.

The Tamagui CLI optimizes your components in-place (or to an output directory), then your bundler processes the already-optimized files.

Learn more: See the CLI Guide for documentation on all CLI commands including check, generate, add, and more.

Props

All compiler plugins accept the same options:

Props

  • config

    string

    Default: 

    './tamagui.config.ts'

    Relative path to your tamagui.config.ts file which should export default the result from createTamagui. Can be set in tamagui.build.ts instead.

  • components

    string[]

    Default: 

    ['tamagui']

    Array of npm modules containing Tamagui components which you'll be using in your app. For example: if you are using the base Tamagui components. This directs the compiler to load and optimize.

  • importsWhitelist

    string[]

    Array of whitelisted file paths (always end in .js) which the compiler may try and import and parse at build-time. It is normalized to ".js" ending for all file extensions (js, jsx, tsx, ts). This usually should be set to something like ['constants.js', 'colors.js'] for example, where you have a couple mostly static files of constants that are used as default values for styles.

  • logTimings

    boolean

    Default: 

    true

    Tamagui outputs information for each file it compiles on how long it took to run, how many components it optimized, and how many it flattened. Set to false to disable these logs.

  • disable

    boolean

    Default: 

    false

    Disable everything - debug and extraction.

  • disableExtraction

    boolean

    Default: 

    false

    Disable extraction to CSS completely, instead fully relying on runtime. Setting this to true speed up development as generally your app will hot reload the Tamagui configuration itself.

  • disableDebugAttr

    boolean

    Default: 

    false

    Disable the data- attributes added in development mode that tie DOM nodes back to their source file and component.

  • disableFlattening

    boolean

    Default: 

    false

    Turns off tree-flattening.

  • outputCSS

    string

    Path to output extracted CSS. When set, the compiler automatically sets TAMAGUI_DID_OUTPUT_CSS which tree-shakes all runtime CSS generation code from your bundle (~6KB gzipped savings).

  • Component discovery

    The compiler resolves every component it meets. Components defined with styled() anywhere in your app are linked across files, and a component imported from any package is evaluated the first time a file uses it as a JSX element or as a styled() base. The result is cached for the life of the dev server or build, and a package bump invalidates the cached plans that lowered against it.

    components is an optional warm-up list. Listing a package evaluates it once when the project loads instead of on first use, which is worth doing for the package most of your files import from. Nothing else is needed for the compiler to optimize components from other packages.

    A package that cannot run in Node during the build (one that touches native modules at import time, for example) is skipped with a debug message, and the elements that use it stay on the runtime path. Set DEBUG=tamagui to see which modules discovery skipped and why.

    Disabling the compiler

    You can disable the compiler optimizations for an entire file with a comment at the top of your file:

    // tamagui-ignore

    You can disable the compiler optimization for a single component with the boolean property disableOptimization:

    import { html } from "@tamagui/tailwind";
    export default () => <html.div disableOptimization />;