πŸ¦Ήβ€β™‚οΈ Twin blends the magic of Tailwind with the flexibility of css-in-js (emotion, styled-components, stitches and goober) at build time.

  • By Ben Rogerson
  • Last update: Dec 30, 2022
  • Comments: 13

twin logo

Twin blends the magic of Tailwind with the flexibility of css-in-js

Total Downloads Latest Release Discord

Demo twin on CodeSandbox β†’


Style jsx elements using Tailwind classes:

import 'twin.macro'

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

Nest Twin’s tw import within a css prop to add conditional styles:

import tw from 'twin.macro'

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

Or mix sass styles with the css import:

import tw, { css } from 'twin.macro'

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

Styled Components

You can also use the tw import to create and style new components:

import tw from 'twin.macro'

const Input = tw.input`border hover:border-black`

And clone and style existing components:

const PurpleInput = tw(Input)`border-purple-500`

Switch to the styled import to add conditional styling:

import tw, { styled } from 'twin.macro'

const StyledInput = styled.input(({ hasBorder }) => [
  `color: black;`,
  hasBorder && tw`border-purple-500`,
])
const Input = () => <StyledInput hasBorder />

Or use backticks to mix with sass styles:

import tw, { styled } from 'twin.macro'

const StyledInput = styled.input`
  color: black;
  ${({ hasBorder }) => hasBorder && tw`border-purple-500`}
`
const Input = () => <StyledInput hasBorder />

How it works

When babel runs over your javascript or typescript files at compile time, twin grabs your classes and converts them into css objects. These css objects are then passed into your chosen css-in-js library without the need for an extra client-side bundle:

import tw from 'twin.macro'

tw`text-sm md:text-lg`

// ↓ ↓ ↓ ↓ ↓ ↓

{
  fontSize: '0.875rem',
  '@media (min-width: 768px)': {
    fontSize: '1.125rem',
  },
}

Features

πŸ‘Œ Simple imports - Twin collapses imports from common styling libraries into a single import:

+ import tw, { styled, css } from 'twin.macro'
- import tw from 'twin.macro'
- import styled from '@emotion/styled'
- import css from '@emotion/react'

🐹 Adds no size to your build - Twin converts the classes you’ve used into css objects using Babel and then compiles away, leaving no runtime code

πŸ›Ž Helpful suggestions for mistypings - Twin chimes in with class and variant values from your Tailwind config:

βœ• ml-7 was not found

Try one of these classes:
ml-0 [0] / ml-1 [0.25rem] / ml-2 [0.5rem] / ml-3 [0.75rem] / ml-4 [1rem] / ml-5 [1.25rem] / ml-6 [1.5rem]
ml-8 [2rem] / ml-10 [2.5rem] / ml-12 [3rem] / ml-16 [4rem] / ml-20 [5rem] / ml-24 [6rem] / ml-32 [8rem]
ml-40 [10rem] / ml-48 [12rem] / ml-56 [14rem] / ml-64 [16rem] / ml-auto [auto] / ml-px [1px]

πŸ’‘ Works with the official tailwind vscode plugin - Avoid having to look up your classes with auto-completions straight from your Tailwind config - See setup instructions β†’

πŸš₯ Over 40 variants to prefix on your classes - The prefixes are β€œalways on” and available for your classes

  • Prefix with hocus: to style hover + focus at the same time
  • Style form field states with checked:, invalid: and required:
  • Stack up variants whenever you need them sm:hover:first:bg-black

Check out the full list of variants β†’

🍱 Apply variants to multiple classes at once with variant groups

import 'twin.macro'

const interactionStyles = () => (
  <div tw="hover:(text-black underline) focus:(text-blue-500 underline)" />
)

const mediaStyles = () => <div tw="sm:(w-4 mt-3) lg:(w-8 mt-6)" />

const pseudoElementStyles = () => <div tw="before:(block w-10 h-10 bg-black)" />

const stackedVariants = () => <div tw="sm:hover:(bg-black text-white)" />

const groupsInGroups = () => <div tw="sm:(bg-black hover:(bg-white w-10))" />

πŸ‘‘ Add vanilla css that integrates with twins features

const setCssVariables = () => <div tw="--base-color[#C0FFEE]" />

const customGridProperties = () => <div tw="grid-area[1 / 1 / 4 / 2]" />

const vendorPrefixes = () => <div tw="-webkit-mask-image[url(mask.png)]" />

πŸ–ŒοΈ Use the theme import to add values from your tailwind config

import { css, theme } from 'twin.macro'

const Input = () => <input css={css({ color: theme`colors.purple.500` })} />

See more examples using the theme import β†’

πŸ’₯ Add !important to any class with a trailing or leading bang!

<div tw="hidden!" /> || <div tw="!hidden" />
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
<div css={{ "display": "none !important" }} />

Add !important to multiple classes with bracket groups:

<div tw="(hidden ml-auto)!" />
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
<div css={{ "display": "none !important", "marginLeft": "auto !important" }} />

Get started

Twin works within many modern stacks - take a look at these examples to get started:

App build tools and libraries

Advanced frameworks

Component libraries

πŸŽ‰  : Fresh example

Community

Drop into our Discord server for announcements, help and styling chat.

Discord

Resources

Special thanks

This project stemmed from babel-plugin-tailwind-components so a big shout out goes to Brad Cornes for the amazing work he produced. Styling with tailwind.macro has been such a pleasure.

Github

https://github.com/ben-rogerson/twin.macro

Comments(13)

  • 1

    Tailwind v3 updates

  • 2

    Global styles get added in the wrong order when using styled-components

    Hi,

    I'm trying to upgrade a project to Tailwind v2 that also uses TailwindUI.

    I'm using react with styled-components.

    I'm running into problems with @tailwindcss/forms that renders my input fields differently depending wether I declare them in the same file as my App component or in a separate file. It seems like the order in the generated style element is not the same.

    I've built an example here: https://github.com/gligoran/twin-react-styled-components-bug (couldn't make it work on codesandbox: https://codesandbox.io/s/twin-react-styled-components-bug-bg82n).

    The main part of the problem is this difference in order as you can see from these 2 screenshots:

    image

    image

  • 3

    Support tailwind plugins (e.g. custom-forms)

    I tried to use https://tailwindcss-custom-forms.netlify.com but seems like it can't find the new classes.

    βœ• β€œform-input” was not found in the Tailwind config.

  • 4

    Tailwind defaults are missing

    Hi again @ben-rogerson in setting up Twin, I'm not seeing some important TW default styles: namely the opacity variables:

      --tw-bg-opacity: 1;
      --tw-text-opacity: 1;
    

    These are needed for theming with custom colors. See https://github.com/adamwathan/tailwind-css-variable-text-opacity-demo/issues/1#issuecomment-770595786 for more detail on that.

    So, your docs don't say what the correct way to import defaults are, as far as I can see? I've tried a number of things. I'm using Gatsby and their tailwind setup docs say to add this import to the top-level file (gatsby-browser.js):

    import 'tailwindcss/dist/base.min.css';
    

    This unfortunately didn't add the opacity resets. I also notice you omit the @tailwind directives css file that Tailwind recommends, often called tailwind.css or just added to global.css in some examples:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    

    Since you omit the latter in your Code Sandbox example I figure we don't need it, but I think we do need some sort of global base css import to get things to work right. If I manually add the two opacity variables to my global css things work correctly, but I imagine there are a lot of other defaults I don't know about yet as well.

    Solution? Thanks!

  • 5

    ReferenceError: styled is not defined

    In an otherwise well-working project with gatsby + styled-components (using tw prop, css prop, and tw import is working fine), I randomly can't get the styled import to work and get ReferenceError: styled is not defined if I try.

    The error doesn't come up always. First I've managed to get it to work somehow. After that, I could get it to fail again with above error by:

    • randomly creating a reference error in an unrelated file (and getting the correct error message).
    • afterwards correcting the reference error
    • now the "ReferenceError: styled is not defined" crops up again.
    • the issue is not fixed by deleting the gatsby cache and building again.
    • it's fixed by replacing the styled import from twin.macro by the one from 'styled-components'
    • it's not fixed by clearing/re-installing all node_modules.

    Sorry I can't pin it down more precisely.

    The setup: gatsby 2.26.1, styled-components 5.2.1, twin.macro 1.12.1. (But playing with some earlier twin version didn't appear to change the behaviour). Running in a yarn workspaces setting if that matters.

    package.json:

      "babelMacros": {
        "twin": {
          "preset": "styled-components",
          "config": "src/tailwind.config.js",
          "autoCssProp": true,
          "debugProp": true,
          "debugPlugins": false,
          "debug": false
        }
      },
    

    I've tried replacing the preset with

          "styled": {
            "import": "default",
            "from": "styled-components"
          },
          "css": {
            "import": "css",
            "from": "styled-components/macro"
          },
          "global": {
            "import": "createGlobalStyle",
            "from": "styled-components"
          }
    

    but that didn't change the outcome.

    Btw: I love the twin project, your pace of adding new goodies is addictive..

  • 6

    String interpolation inside tw macro doesn't work

    Hi!

    I have a problem with string interpolation in tw macro. For example I need to customize border color, so I'm trying to do it like this:

    export const PrimaryButton = styled(Button)`
      ${({ color }) => tw`hover:border-${color ?? 'blue'}`};
    `;
    

    But I get the following error: βœ• Class β€œhover:border-” shouldn’t have a trailing dash.

    It also doesn't work without variables:

    export const PrimaryButton = styled(Button)`
      ${({ color }) => tw`hover:border-${'blue'}`};
    `;
    
  • 7

    Issues pre-bundling twin components with Rollup and importing them

    Hey,

    This may be a dumb question.

    If i publish a package to NPM ... say button that i use this macro within and then publish. If i then import this package with the button would this still work?

    for example...

    import tw from "twin.macro";
    
    const Button = () => <button tw="w-32 m-10 p-5 block opacity-50 hover:opacity-100">ddd</button>
    
    export default Button;
    

    nextjs site

    import Button from "package";
    
    const page = () => <Button />
    
    export default page;
    

    should that work or am i missing the point the macros in that it is only compile time... I use rollup for button example

  • 8

    styled-components: _cssPropImport is not defined

    Hey,

    1. I get this error in production only on vercel: _cssPropImport is not defined
    2. Using styled components
    3. Creating a test repo to show you how, in the meantime you can found the error on https://app.axolo.co
  • 9

    Q: Why is emotion necessary with CRA?

    The top of the readme there is the following simple way which is absolutely amazing at a first sight.

    import tw from 'twin.macro'
    
    const default () => <input tw="border hover:border-black" />
    

    Furthermore, there is also this sentence which would give me the impression that without conditionals I can use macro only.

    For features like conditional styling and vanilla css, Twin works with styling libraries like πŸ‘©β€πŸŽ€ emotion or πŸ’… styled-components

    Unfortunately, when I try that in a clean CRA project, the output I get is like this and that's where the magical feeling is kinda lost.

    <input css="[object Object]" />
    

    To make that work I have to include this ceremony in each file that needs some styling.

    /** @jsx jsx */
    import { jsx } from '@emotion/core'
    import tw from 'twin.macro'
    

    Is there some better way I am simply not seeing?

  • 10

    Error "Identifier '_styled' has already been declared" with styled-components + ts + next.js

    Looks like the compiler duplicates the import statement of styled-components with the one created by the babel macro when mixing <div tw="..." /> usage with tw.div``.

    This happens when using:

    • twin.macro
    • Nextjs
    • styled-components
    • Typescript

    Error:

    Module parse failed: Identifier '_styled' has already been declared (3:7)
    File was processed with these loaders:
     * ./node_modules/@next/react-refresh-utils/loader.js
     * ./node_modules/next/dist/build/webpack/loaders/next-babel-loader.js
    You may need an additional loader to handle the result of these loaders.
    | import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
    | import _styled from "styled-components";
    > import _styled from "styled-components";
    

    To reproduce:

    • With error: https://github.com/lopezjurip/twin.macro-nextjs-styled-ts-error
    • With commented lines to make it work: https://github.com/lopezjurip/twin.macro-nextjs-styled-ts-error/commit/507900c0e295bab5a5408cd8e98fbaffdda2ccf8

    Maybe this is related to https://github.com/ben-rogerson/twin.macro/issues/192?

  • 11

    Importing in Typescript errors ts(2614)

    I finally started trying out twin (you recommended it on reddit a few weeks ago!) having issues starting up in typescript however.

    I followed the installation guide for CRA, I've made two code sandboxes setting up: Plain JS (Working): https://codesandbox.io/s/lucid-kapitsa-cjy3y?file=/src/App.js TS (Errors): https://codesandbox.io/s/busy-moser-yxl7g?file=/src/App.tsx

    As you can see the JS works fine, and the TS does display the elements styled but there is a TS error for the imports Module '"../node_modules/twin.macro/types"' has no exported member 'tw'. Did you mean to use 'import tw from "../node_modules/twin.macro/types"' instead?ts(2614) I'm assuming it can't find the type definitions? I know you say you've got them built in, not sure if I missed something in installation.

  • 12

    importing tw in a dynamic route throws module not found error for stitches.config.ts

    I'm getting following error when importing and using tw in a dynamic route.

    ./pages/[username]/transactions.tsx
    Module not found: Can't resolve '..\..\stitches.config.ts' in 'C:\code\project\pages\[username]'
    Did you mean './..\..\stitches.config.ts'?
    Requests that should resolve in the current directory need to start with './'.
    Requests that start with a name are treated as module requests and resolve within module directories (node_modules).
    If changing the source code is not an option there is also a resolve options called 'preferRelative' which tries to resolve these kind of requests in the current directory too.
    

    Found preferRelative in webpack's documentation here. But setting it in next.config.js did not resolve the issue.

  • 13

    Add Solid.js support

    The solid-styled-component package for Solid is a thin wrapper around Goober.

    This patch adds preset: solid, which loads the Goober wrappers and otherwise behaves as Goober would.

    A small site has been written using this PR, as well as this recently merged Solid PR for createGlobalStyles support. If you edit pages/index/index.jsx and comment out lines 30 through 95, you can easily play with the client without any server errors in the console.

    I'm not sure exactly how you'd like to see these changes implemented and there are currently caveats. At the moment, however, I'd say it's entirely usable as a feature. Whatever you advise is fine.

    Here are the current caveats. The following pattern works:

    import tw, { styled } from "twin.macro";
    
    export default function App(props) {
      const Button = styled("button")`
        ${() => tw`text-white`}
      `;
      return <Button {...props} />;
    }
    

    The following will not:

    import "twin.macro";
    
    export default function App(props) {
      return <button {...props} tw="text-white" />;
    }
    

    However, the following also works:

    export default function App(props) {
      const ToggleButton = styled("button")`
        ${() => (props.active ? tw`text-white` : tw`text-gray-700`)}
      `;
      return <ToggleButton {...props} />;
    }
    
    <ToggleButton active={state.volume > 0} onClick={click}>
      <Svg alt="speaker" />
    </ToggleButton>
    

    Where Svg also demonstrates the css escape hatch:

    import tw, { css } from "twin.macro";
    
    export default function App(props) {
      const svg = css`
        ${() => tw`
          h-8
          w-8
          fill-current
          stroke-current
          inline-block
          text-center 
          bg-cover
        `}
      `;
      return (
        <svg {...props} class={svg}>
          <use xlink:href={`#svg-${props.alt}`}></use>
        </svg>
      );
    }
    

    And most importantly GlobalStyles works:

    import { createGlobalStyles } from "solid-styled-components";
    import tw, { GlobalStyles as BaseStyles } from "twin.macro";
    
    export default function App() {
      const CustomStyles = createGlobalStyles`   
        body { 
          ${() => tw`subpixel-antialiased`}
        }
        video {
          ${() => tw`h-screen w-full bg-black z-0`}
        }
        video::-webkit-media-controls {
          ${() => tw`hidden`}
        }
      `;
    
      return (
        <>
          <BaseStyles />
          <CustomStyles />
        </>
      );
    }
    

    The only setup needed is package.json:

    {
      "babelMacros": {
        "twin": {
          "preset": "solid"
        }
      },
      "devDependencies": {
        "vite": "^2.2.1",
        "vite-plugin-babel-macros": "^1.0.5",
        "vite-plugin-solid": "^1.5.1"
      },
      "dependencies": {
        "solid-js": "^0.26.1",
        "solid-styled-components": "^0.26.1",
        "twin.macro": "^2.3.3"
      }
    }
    

    and vite.config.js:

    import { defineConfig } from "vite";
    import solid from "vite-plugin-solid";
    import macros from "vite-plugin-babel-macros";
    
    export default defineConfig({
      plugins: [ solid(), macros(), ],
    
      build: {
        target: "esnext",
        polyfillDynamicImport: false,
      },
    });