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 _document
page. 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> ); }
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;
@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;
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.