# twin.macro: Tailwind classes compiled into css-in-js objects

> twin.macro is a Babel macro that turns Tailwind class strings into css-in-js objects at build time, so you get Tailwind's utility vocabulary inside emotion, styled-components, stitches, goober or solid-styled-components without shipping a Tailwind runtime. It suits teams already committed to css-in-js; it is a poor fit for anyone who wants Tailwind's generated stylesheet.

**ben-rogerson/twin.macro** — 🦹‍♂️ Twin blends the magic of Tailwind with the flexibility of css-in-js (emotion, styled-components, solid-styled-components, stitches and goober) at build time.

- Repository: https://github.com/ben-rogerson/twin.macro
- Stars: 8,027 · Forks: 182
- Language: TypeScript
- License: MIT
- Published: 2026-09-22 · Updated: 2026-11-03 · Language: en
- Canonical page: https://hysenlabs.com/projects/ben-rogerson-twin-macro

## What twin.macro solves, and for whom

Tailwind normally works by scanning your source for class names and emitting a stylesheet. That model assumes class attributes. If your components are styled with emotion or styled-components, you are writing style objects or template literals, and Tailwind's utility names are not available inside them. twin.macro closes that gap by letting you write `tw`border hover:border-black`` inside a component and having the class string replaced at compile time.

The audience is narrow and specific: React, Preact or Solid projects that have already chosen a css-in-js library and do not want to add a second styling system. The README lists adapters for emotion, styled-components, solid-styled-components, stitches and goober, and the examples repository covers Parcel, Webpack, Vite, Create React App, Preact and Next.js. If you are starting fresh with no css-in-js dependency, this is an extra layer you do not need.

## How the Babel macro rewrites your classes

The mechanism is a Babel macro. When Babel runs over your JavaScript or TypeScript, twin.macro intercepts the tagged template and converts the classes into a css object. The README shows the transformation directly: `tw`text-sm md:text-lg`` becomes an object with `fontSize: '0.875rem'` and a `@media (min-width: 768px)` key holding `fontSize: '1.125rem'`. That object is then handed to whichever css-in-js library you configured.

Because the work happens at compile time, the macro itself is not in the output. The README states that Twin adds no size to your build and leaves no runtime code. The published package reflects this: `package.json` ships only `macro.js` and `types/index.d.ts`, so the artifact you install is the macro plus its type declarations, not a runtime library. Your bundle still contains the css-in-js library you chose, and twin.macro does not change that cost.

The macro also reads your Tailwind config for suggestions. A mistyped class such as `ml-1.25` produces a list of nearby classes with their resolved values, which is a compile-time error message rather than a silent no-op.

## Installing twin.macro and a first component

The README points to the twin.examples repository for setup rather than giving one canonical install command, and the repository's `engines` field requires Node 20 or newer. The macro is consumed through Babel, so the practical first step is installing the package alongside the css-in-js library you intend to use. A minimal install looks like this:

```bash
npm install --save-dev twin.macro
npm install @emotion/react
```

The package is built as a Babel macro (`build:macro` runs microbundle against `src/macro.ts`), and the examples it links to are per-stack, so follow the example matching your bundler. Once wired up, the smallest useful component uses the `tw` prop:

```js
import 'twin.macro'

const Input = () => <input tw="border hover:border-black" />
```

If you prefer the tagged template form, the README gives this example, where the classes are passed through the `tw` import and merged conditionally:

```js
import tw from 'twin.macro'

const Input = ({ hasHover }) => (
  <input css={[tw`border`, hasHover && tw`hover:border-black`]} />
)
```

After a build, the class strings should be gone from the emitted code and replaced by css objects. If they are still present as strings, the macro did not run and the Babel configuration is the place to look.

## Variant groups, the theme import and the bang suffix

The feature that distinguishes twin.macro from a plain class-to-object converter is variant grouping. Instead of repeating a prefix on every class, you can write `hover:(text-black underline)` or `sm:(w-4 mt-3)`, and the README also shows nesting such as `sm:(bg-black hover:(bg-white w-10))`. This exists because css-in-js style objects have no equivalent of Tailwind's prefix syntax, and expanding `hover:` by hand across five classes is where mistakes happen.

The `theme` import reads values out of your Tailwind config at compile time. The README example is `css({ color: theme`colors.purple.500` })`, which means the config is the single source of truth for colours and spacing rather than a duplicated constant.

A trailing or leading `!` sets `!important` on a class, and bracket groups extend it: `(hidden ml-auto)!` becomes `display: none !important` and `marginLeft: auto !important` in the same object. That is a blunt tool, and the README presents it without qualification. Reaching for it usually means a specificity conflict you have not resolved elsewhere, and in a css-in-js codebase you often can.

## Where twin.macro is the wrong choice

The macro only runs if Babel processes the file. A project on SWC, esbuild's native transform, or a framework that has moved its transform pipeline away from Babel will not get the rewrite without adding a Babel pass, which is real build-time cost for a styling convenience.

Tailwind version compatibility is the second boundary. The newest release listed is 3.4.1 from 2024-01-19. Anyone on a Tailwind release newer than what the macro understands should confirm support before committing.

Finally, if you want a stylesheet, twin.macro is the wrong tool entirely. It produces style objects, so there is no generated CSS file to inspect, cache or ship separately. Teams whose deployment or CSP setup depends on a static stylesheet lose that. The last push to the repository was on 2026-03-22, so the codebase is not abandoned, but the release cadence is slow.

## twin.macro compared with plain Tailwind and with vanilla-extract

Plain Tailwind takes the opposite approach: it scans source files for class names and emits a stylesheet, and your components carry `className` attributes. The difference is where the class names resolve. With Tailwind, resolution happens in a build step that produces CSS, and anything dynamic has to be handled by composing class strings. With twin.macro, resolution happens per file inside Babel, and the result is a JavaScript object, which is why conditions such as `hasHover && tw`hover:border-black`` work naturally.

Against a zero-runtime css-in-js compiler such as vanilla-extract, the trade-off is vocabulary versus typing. vanilla-extract makes you define typed style functions and does not give you Tailwind's class names; twin.macro gives you the class names but leaves the runtime to whatever library you paired it with. Neither adds runtime styling code of its own, but twin.macro's output still depends on emotion, styled-components, stitches or goober being present.

## Licence, maintenance and upgrade cost

twin.macro is MIT licensed, and the repository contains a `LICENSE` file at the top level. MIT is permissive, so the practical implication for most teams is attribution rather than any restriction on commercial use. This is a description of the licence identifier, not legal advice; check the file itself if your organisation has specific requirements.

The maintenance picture is mixed. The repository is not archived and the last push was on 2026-03-22. The most recent release listed is 3.4.1 from 2024-01-19, with 3.4.0 in 2023-07-25 and 3.3.1 in 2023-04-14 before it. Commits continue while tagged releases are infrequent, so pinning a version and reading the changelog before upgrading is the sensible default. The `engines` field requires Node 20 or newer, which is a hard floor for your build environment.

Upgrade cost concentrates in two places: the Tailwind config the macro reads, and the css-in-js library you paired it with. Changing either can change the generated objects, and because there is no runtime to patch, a mismatch shows up as a build error or as styles that silently resolve differently.

## Conclusion

Adopt twin.macro when your components already live in emotion, styled-components, stitches or goober and you want Tailwind's class vocabulary without a generated stylesheet; skip it when you want Tailwind's own build output or your toolchain cannot run a Babel macro. Verify first that your Tailwind version is supported, since the README examples do not pin one, and that a plain Babel build of one component produces the css object you expect before wiring it into a real app.

## FAQ

### What is twin.macro and what does it do?

It is a Babel macro that converts Tailwind class strings into css objects at compile time and passes them to a css-in-js library such as emotion, styled-components, stitches or goober. The README states that it leaves no runtime code in your build.

### How do I install twin.macro?

The README points to the twin.examples repository for per-stack setup rather than giving one install command, and the package requires Node 20 or newer. The package itself ships only macro.js and the type declarations, so the working configuration lives in your Babel setup.

### Is twin.macro a replacement for Tailwind?

No. It reads your Tailwind config and resolves the same class names, but it emits css objects instead of a stylesheet, so there is no generated CSS file. Plain Tailwind and twin.macro resolve classes in different places and produce different artifacts.

### Does twin.macro work with Tailwind 4?

The newest release listed is 3.4.1 from 2024-01-19. Confirm support against the repository before adopting it on a newer Tailwind release.

### What are the alternatives to twin.macro?

Plain Tailwind emits a stylesheet from class names in your source, and zero-runtime css-in-js compilers such as vanilla-extract use typed style functions instead of Tailwind vocabulary. twin.macro keeps Tailwind class names but still depends on a css-in-js library at runtime.

### Can I mix twin.macro classes with regular CSS in the same component?

Yes. The README shows mixing sass-style declarations with the tw import inside a css template literal, and the styled import supports the same pattern with a conditional expression inside the interpolation.

## Sources

- [ben-rogerson/twin.macro on GitHub](https://github.com/ben-rogerson/twin.macro)
- [Issues](https://github.com/ben-rogerson/twin.macro/issues)
- [License: MIT](https://github.com/ben-rogerson/twin.macro/blob/master/LICENSE)
- [README](https://github.com/ben-rogerson/twin.macro/blob/master/README.md)
- [Releases](https://github.com/ben-rogerson/twin.macro/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ben-rogerson-twin-macro
