code/tamagui.dev/data/docs/intro/compiler-install.mdx
See the Benchmarks or a more 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 - like a div on web, or plain React Native
View on native:
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.
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 — they'll 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.
There are plugins for a variety of bundlers, or you can use the @tamagui/cli
to compile in-place:
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',
// optional advanced optimization of styled() definitions within your app itself, not just ones in your components option
// default is false
enableDynamicEvaluation: false,
}
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:
See the Vite guide for more complete setup.
<Notice theme="yellow"> `@tamagui/vite-plugin` is ESM-only. Your project must have `"type": "module"` in its `package.json` (or use `.mjs`/`.mts` config files). </Notice>Add @tamagui/vite-plugin and update your vite.config.ts. If you have a
tamagui.build.ts, no options are needed:
import { tamaguiPlugin } from '@tamagui/vite-plugin'
export default defineConfig({
plugins: [
// reads from tamagui.build.ts automatically
tamaguiPlugin(),
],
})
Or pass options inline:
import { tamaguiPlugin } from '@tamagui/vite-plugin'
export default defineConfig({
plugins: [
tamaguiPlugin({
config: 'src/tamagui.config.ts',
components: ['tamagui'],
disableExtraction: true,
}),
],
})
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.
Note that the @tamagui/babel-plugin is completely optional, and on native
Tamagui doesn't optimize as much as on web, so leaving it out is actually
recommended to start. If later on you feel the need for a bit more speed, you
can try adding it.
yarn add @tamagui/babel-plugin
Add to your babel.config.js. With a tamagui.build.ts, you can pass no
options:
module.exports = {
plugins: [
// reads from tamagui.build.ts automatically
'@tamagui/babel-plugin',
],
}
Or pass options inline:
module.exports = {
plugins: [
[
'@tamagui/babel-plugin',
{
components: ['tamagui'],
config: './tamagui.config.ts',
importsWhitelist: ['constants.js', 'colors.js'],
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
},
],
],
}
Check out the Expo guide for more information on setting up Expo. It's as simple as adding the babel plugin.
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.
yarn add -D @tamagui/cli
Create a tamagui.build.ts if you haven't already (see
above).
Add a build script to your package.json:
{
"scripts": {
"build": "tamagui build ./src -- next build"
}
}
# 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
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:
components arrayThe CLI automatically handles platform-specific files (.web.tsx,
.native.tsx, .ios.tsx, .android.tsx):
.web.tsx extensions are optimized for web only.native.tsx, .ios.tsx, or .android.tsx extensions are
optimized for native only.tsx) without platform-specific versions are optimized for all
platforms.web.tsx and .native.tsx exist, the base .tsx file is skippedThe 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'
This works with any build tool - just 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.
All compiler plugins accept the same options:
<PropsTable
data={[
{
name: 'config',
required: false,
type: 'string',
typeSimple: 'enum',
default: "'./tamagui.config.ts'",
description:
'Relative path to your tamagui.config.ts file which should export default the result from createTamagui. Can be set in tamagui.build.ts instead.',
},
{
name: 'components',
required: false,
type: 'string[]',
typeSimple: 'enum',
default: "['tamagui']",
description: 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.,
},
{
name: 'importsWhitelist',
required: false,
type: 'string[]',
typeSimple: 'enum',
description: 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.,
},
{
name: 'logTimings',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'true',
description:
'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.',
},
{
name: 'disable',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description: 'Disable everything - debug and extraction.',
},
{
name: 'disableExtraction',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description:
'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.',
},
{
name: 'disableDebugAttr',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description:
'If enabled along with disableExtraction, all parsing will turn off. Normally turning off disableExtraction will keep the helpful debug attributes in DOM',
},
{
name: 'disableFlattening',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description: 'Turns off tree-flattening.',
},
{
name: 'enableDynamicEvaluation',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description:
'(Experimental) Enables further extracting of any styled component, even if not in your components. See below for more information.',
},
{
name: 'outputCSS',
required: false,
type: 'string',
typeSimple: 'enum',
description:
'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).',
},
]}
/>
By default the Tamagui compiler only optimizes styled expressions found in the
modules defined by your components config. This means if you do an inline
styled() inside your actual app directory, it will default to runtime style
insertion.
This is typically Good Enough™️. As long as you define most of your common components there, you'll get a very high hit rate of compiled styles being used and runtime generation being skipped, as atomic styles with your design system tokens will be mostly pre-generated.
Tamagui has experimental support for loading any component, even if it occurs
somewhere outside your configured components modules. This is called "dynamic
loading", for now. You can enable it with the setting enableDynamicEvaluation
as seen above in the props table.
The way it works is, when the compiler detects a styled() expression outside one of the defined component directories, it will run the following:
babel transform to force all
top-level variables to be exported.esbuild and bundle the entire file to a temporary file in the
same directory, something like .tamagui-dynamic-eval-ComponentName.jsYou may see why this is experimental. It's very convenient as a developer, but has a variety of edge cases that can be confusing or breaking, and we want to avoid installation woes. Though it does continue on error and work generally, it outputs warnings in Webpack currently due to our plugin not properly indicating to Webpack about the new files (a fixable bug), which causes big yellow warning output and a cache de-opt.
We're leaving this feature under the environment variable while it matures. Let us know if you find it useful.
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 { View } from '@tamagui/core'
export default () => <View disableOptimization />
If you want autocompleted imports of react-native without having to install
all the weight of react-native, you can set react-native version to 0.0.0,
and add @types/react-native at the latest version.