
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;
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.
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"] }
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> ); } }
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> ); }
classNameIf 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;
@emotion/styledA 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;
css PropA 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;
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.
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.