Skip to main content
  1. Posts/

Extending Create React App with Babel Macros

I have a co-dependent relationship with Create React App. When we’re good, we’re really good. When I have an unmet need — when I want to customize some part of the build process — I do my best to make CRA work. And when it doesn’t, for whatever reason, I eject and fall back to Webpack, Babel, & friends, resentful that CRA has let me down. Recently, though, I discovered that you can customize Babel configuration in a Create React App projects without ejecting.

Last month I was working on some shared React components and ran into this again: I wanted to use Tailwind.css — fine — but I also wanted to include it in the resulting Javascript files as CSS-in-JS1. I initially despaired, thinking I’d have to eject the components in order to customize the Babel configuration.

And then I discovered Babel Macros, which — lo and behold — are supported by CRA since 2018!

Babel Macros are exactly what they sound like if you’re familiar with Lisp-y languages: they’re code that generates different code. In other words, they give you a functional interface to Babel’s transpiling capabilities. This allows you to write “normal” Javascript (or Typescript) code that CRA can process, but when that code is executed, it hooks into Babel’s runtime.

For my Tailwind CSS-in-JS it looks like this.

First, I tell Babel (and by extension CRA) that I want to enable macros, by adding macros to the list of plugins in my .babelrc:

{
   "presets": ["@babel/env", "@babel/react", "@babel/typescript"],
   "plugins": [
     "@babel/proposal-class-properties",
     "@babel/syntax-object-rest-spread",
     "macros"
   ]
}

Then, when I want to use Tailwind-in-JS, I import the macro and use it to tag a string.

import tw from "@tailwindcssinjs/macro";

...

// in my react component
return (
    <div
      style={tw`font-normal text-sm text-gray-700`}
    >...</div>
);

Note that I’m setting what looks like the Tailwind class list as the style property of my element: that’s because tw is actually a function that utilizes Babel’s macro functionality to map the classes to their underlying CSS properties.

With this small bit of configuration in place, running the CRA build script results in pure Javascript I can use in my downstream projects, including the necessary CSS.

There are other advantages, too: someone reading this code can now “follow their nose” to figure out what’s going on. One of the most persistent problems I’ve encountered when approaching a large codebase is understanding how the source is built: where does a dependency come from? how is the code compiled? where — why!? — does a transformation happen? This component now answers those questions for me: the use of Babel (and the macro) is explicit.


  1. There’s probably another post here: getting shared components to work with external CSS has been a real pain for me. ↩︎