TutorialsCourses

Setup Emotion to use Tailwind classes in Next.js

Why?

Rather than utilizing class names and handling the optimization of which Tailwind classes to include we can leverage the style attributes but render those values with Emotion.

We can also integrate dynamic and existing styling with Emotion. Further more because the styles are dynamically assessed we can check that the style actually exists from Tailwind.

If there is ever a className you're trying to use that isn't supplied by Tailwind, or a style that was created from your modifications to Tailwind config an error will be thrown.

This allows you to verify that all your styling and classes exist in your whole app.

We will leverage the package https://github.com/Arthie/xwind;

Setup the Dependencies and Base

Rather than provide an install this is the package.json there are a lot of packages that all work in conjunction with each other.

{
  "name": "tailwindcss-emotion",
  "version": "1.0.0",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "build:base-css": "tailwindcss build ./styles/tailwind.base.css -o ./styles/base.css"
  },
  "dependencies": {
    "@emotion/css": "^11.0.0",
    "@emotion/react": "^11.1.2",
    "@emotion/server": "^11.0.0",
    "@emotion/styled": "^11.0.0",
    "next": "latest",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  },
  "devDependencies": {
    "@babel/core": "^7.12.10",
    "@emotion/babel-preset-css-prop": "^11.0.0",
    "autoprefixer": "^10.1.0",
    "babel-plugin-macros": "^3.0.0",
    "postcss": "^8.2.1",
    "tailwindcss": "^2.0.2",
    "xwind": "^0.7.4"
  },
  "license": "MIT"
}

This is just a basic Tailwind config. Setup yours for whatever you need. We'll use the default color pallete with some additions. So we import the colors package from tailwindcss

// tailwind.config.js
const colors = require("tailwindcss/colors");

module.exports = {
  theme: {
    colors: {
      transparent: "transparent",
      current: "currentColor",
      black: "#000",
      white: "#fff",
      ...colors,
    },
  },
  plugins: [],
  xwind: {
    mode: "objectstyles",
  },
};

If you make any changes here run yarn build:base-css to rebuild and regenerate the base css.

Config Babel

Now create a custom .babelrc file at the root, and add next/babel as the first preset. This is essential. Then add our @emotion/babel-preset-css-prop to the preset which will automatically look for css={} props and swap over the jsx pragma for you.

So you do not need to manually add /** @jsx jsx */ to the top of the file with the jsx import.

Also add babel-macros so we can transform our Tailwind classnames into CSS that will be passed into Emotion.

// .babelrc
{
  "presets": ["next/babel", "@emotion/babel-preset-css-prop"],
  "plugins": ["macros", "xwind/babel"]
}

Add in Custom _document Page

This is semi-optional, but probably recommended. If you plan to use @emotion/css at all then this it is required to setup a custom _documentpage. This will allow you to render and extract all the styling at SSR time to ensure it's all there at initial render time.

Also to include font stylings that are recommended from Tailwind.

import Document, { Html, Head, Main, NextScript } from "next/document";
// Required for @emotion/css
import { extractCritical } from "@emotion/server";

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    const page = await ctx.renderPage();
    const styles = extractCritical(page.html);
    return { ...initialProps, ...page, ...styles };
  }

  render() {
    return (
      <Html lang="en">
        <Head>
          <style
            data-emotion-css={this.props.ids.join(" ")}
            dangerouslySetInnerHTML={{ __html: this.props.css }}
          />
          <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Add in Custom _app Page

The custom _app will allow us to include global CSS, but not have it imported as css modules. Next.js will automatically include and insert a link tag in the head for us.

import Head from "next/head";
import { Fragment } from "react";
import "../styles/base.css";

export default function MyApp({ Component, pageProps }) {
  return (
    <Fragment>
      <Head>
        <title>Tailwindcss Emotion</title>
      </Head>
      <Component {...pageProps} />
    </Fragment>
  );
}

Style Option One with className

If you do not want to use the @emotion/babel-preset-css-prop plugin this is an option. Using @emotion/css and wrapping the xw macro to turn the combination of stylings into a stringified className.

Also if you need to combine styles you can use the cx export from @emotion/css to dynamically apply styling. This is the most verbose option of styling.

import xw from "twin.macro";
import { css, cx } from "@emotion/css";

const base = xw`relative flex justify-center w-64 min-w-full px-4 py-2 text-sm font-medium leading-5 text-white border border-transparent rounded-md `;
const styles = {
  cssBase: css(base),
  cssButton: css(xw`
    bg-gray-600
    hover:bg-gray-500
    focus[outline-none border-gray-700 ring]
    active:bg-gray-700
    transition duration-150 ease-in-out
  `),
};

const Index = () => (
  <div css={xw`grid items-center justify-center h-screen`}>
    <button className={cx(styles.cssBase, styles.cssButton)}>
      Emotion + Tailwind
    </button>
  </div>
);

export default Index;

Style with @emotion/styled

A typical styling from @emotion is to use the styled package and create styled components. These are components that can be used as normal React components but have their encapsulated styling applied.

You can additionally supply a className or even css prop to add extend and add more styling.

import xw from "twin.macro";
import { styled } from "@/stitches.config";

const Button = styled.button(xw`
  bg-indigo-600
  hover:bg-indigo-500
  focus[outline-none border-indigo-700 ring]
  active:bg-indigo-700
  transition duration-150 ease-in-out
`);

const base = xw`relative flex justify-center w-64 min-w-full px-4 py-2 text-sm font-medium leading-5 text-white border border-transparent rounded-md `;

const Index = () => (
  <div css={xw`grid items-center justify-center h-screen`}>
    <Button css={base}>Emotion + Tailwind</Button>
  </div>
);

export default Index;

Style with css Prop

A handy method that @emotion/babel-preset-css-prop allows is to add a css prop. This means we do not need to import anything but our tailwind macro. It will handle converting our supplied styles to a className. So it might appear as a css prop here but will be compiled to className prop.

Further we don't even need cx here as it accepts an array of styles to apply, and this can even be dynamically changed.

import xw from "twin.macro";

const base = xw`relative flex justify-center w-64 min-w-full px-4 py-2 text-sm font-medium leading-5 text-white border border-transparent rounded-md `;
const styles = {
  base: base,
  button: xw`
    bg-teal-600
    hover:bg-teal-500
    focus[outline-none border-teal-700 ring]
    active:bg-teal-700
    transition duration-150 ease-in-out
  `,
};

const Index = () => (
  <div css={xw`grid items-center justify-center h-screen`}>
    <button css={[styles.base, styles.button]}>Emotion + Tailwind</button>
  </div>
);

export default Index;

VSCode Extension

If you want an even easier time writing your styling install the VSCode extension https://marketplace.visualstudio.com/items?itemName=Arthie.vscode-xwind

It will autocomplete for the xw macro.

Conclusion

Overall this might be over kill, but it is a great way to add autocompletion, and verify that all the classes inside of your app are valid. Further it will only include the exact styling you need, so no need to analyze your code and remove classNames from Tailwind after the fact. It does that automatically for us.