You set up a fresh Vite React project, install Tailwind, and drop in Twin Macro to handle CSS-in-JS. The dev server runs perfectly, but the moment you hit build or add TypeScript, the terminal floods with macro errors. Configuring Vite with Babel macros requires specific tweaks that official documentation often skips. Here is the exact setup to get Twin Macro running with Emotion and TypeScript without facing those notorious build failures.

  • Core Stack: Vite 5, React, Tailwind CSS v3.4.x
  • Recommended Engine: Emotion
  • Required Plugin: vite-plugin-babel-macros
  • Common Blocker: TypeScript module resolution for the css prop

Emotion vs. Styled-Components: Which One to Choose?

Most developers get stuck deciding between the two main CSS-in-JS engines for Twin Macro. Emotion has slightly better integration with Babel macros and handles server-side rendering without massive configuration overhead. The compilation speed is visibly faster. Go with Emotion unless your legacy codebase explicitly demands Styled-Components.

Step 1: Vite React Project and Dependencies Setup

Start by generating a standard Vite React TypeScript template. Open your terminal and run the scaffolding command.

npm create vite@latest my-app --template react-ts

Navigate into your project folder and install the core styling packages. You need Twin Macro, Tailwind CSS, and the Emotion packages.

npm i twin.macro tailwindcss @emotion/react @emotion/styled

Next, install the development dependencies. These are critical for making Vite understand Babel macros during the build step.

npm i -D vite-plugin-babel-macros babel-plugin-twin

Initialize Tailwind to generate your config file.

npx tailwindcss init -p

Open the generated tailwind.config.js and set the content paths so Tailwind knows which files to scan for class names.

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Step 2: Configuring vite.config.ts and Babel Macros

Vite uses ESBuild under the hood instead of Webpack. This architecture makes it blazing fast, but it means Babel macros will not work out of the box. You must explicitly inject the macro plugin into your Vite configuration.

Update your vite.config.ts file exactly like this:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import macrosPlugin from 'vite-plugin-babel-macros'
export default defineConfig({
  plugins: [
    react(),
    macrosPlugin()
  ],
  define: {
    'process.env': {}
  }
})

Notice the process.env definition at the bottom. Vite environments do not natively support Node variables. Adding this empty object prevents runtime crashes when third-party libraries look for Node environments.

Create a .babelrc file in your project root to handle the auto-injection of imports. This eliminates the need to manually import React or Emotion in every component.

{
  "plugins": [
    "babel-plugin-twin",
    "babel-plugin-macros"
  ]
}

Step 3: Global Styles and TypeScript Declarations

Twin Macro requires global base styles to function. Without them, specific Tailwind utility classes simply will not render.

Create a GlobalStyles.tsx file in your src directory.

import React from 'react'
import { GlobalStyles as BaseStyles } from 'twin.macro'
const GlobalStyles = () => (
  <>
    <BaseStyles />
  </>
)
export default GlobalStyles

Import this component directly into your main.tsx or App.tsx file and place it at the top of your component tree.

TypeScript will still complain about the tw and css props missing from standard HTML elements. You need a dedicated declaration file to extend the React types. Create a twin.d.ts file in your src folder.

import 'twin.macro'
import { css as cssImport } from '@emotion/react'
import styledImport from '@emotion/styled'
declare module 'twin.macro' {
  const styled: typeof styledImport
  const css: typeof cssImport
}
declare module 'react' {
  interface HTMLAttributes<T> extends DOMAttributes<T> {
    tw?: string
    css?: any
  }
}

Handling Common Vite and Twin Macro Errors

Even with a flawless setup, version mismatches or caching issues can trigger specific console errors. Here are the fastest ways to resolve the top three blockers.

Fixing "process is not defined"

This happens right after starting the dev server. Vite runs in the browser context, lacking Node's process object. Always verify that define: { 'process.env': {} } is present inside your vite.config.ts. If the error persists, clear the .vite cache folder inside node_modules.

Resolving MacroError: babel-plugin-macros Not Configured

This error strikes during the production build. The build tool cannot locate your Babel configuration. Ensure your .babelrc file is exactly in the root directory, right next to your package.json file. Moving it inside the src folder breaks the resolution path.

TypeScript ts(2614) Error Fix

Your IDE throws a red underline claiming the module has no exported member css. This is strictly a TypeScript compiler issue. Open your tsconfig.json and ensure you have jsxImportSource set to @emotion/react under compiler options.

{
  "compilerOptions": {
    "jsxImportSource": "@emotion/react"
  }
}

Tailwind CSS v4 Incompatibility

The Tailwind team completely rewrote the core engine for version 4. Twin Macro heavily relies on the internal JavaScript API of Tailwind v3 to transform classes into CSS objects at build time. Upgrading to Tailwind v4 will instantly break your Twin Macro setup.

Always lock your dependency to v3.4.x in your package.json. Do not use the caret symbol for major version bumps here. If you are already working with Tailwind v4 features like CSS-native custom colors, keep that project separate from any Twin Macro setup.

When NOT to Use Twin Macro

Twin Macro is powerful for small to medium projects, but it introduces overhead during the build step because Babel parses every single component. If you are building a massive application with hundreds of complex components, build times will degrade significantly. The engineering team at Railway removed it from their monolithic frontend for this exact reason.

For ultra-large codebases, pure Tailwind CSS or zero-runtime tools like Vanilla Extract is a safer architectural decision. For everything else, the setup above handles the real pain points that trip up most developers on their first attempt. Start with the vite.config.ts change and the twin.d.ts declarations. Those two files resolve about 80% of reported setup issues.