Styling with Twind

The tw function

At the heart of Twind is the tw function. This function works by:

  • Interpreting and normalizing rules that are provided to it and compiling them into CSS styles
  • Injecting those styles into the head of the document
  • Returning class name(s) that associate the injected styles to the element

Take this simple example:

document.body.innerHTML = `<p class=${tw`text-blue-500`}>Hello twind!</p>`

The class name of text-blue-500 would be returned to the p element's class attribute and a stylesheet similar to the example below would be injected in the head of the document:

.text-blue-500 {
  --tw-text-opacity: 1;
  color: rgba(59, 130, 246, var(--tw-text-opacity));
}

The tw function accepts a variety of inputs (inspired heavily by clsx):

  • Template Literal (recommended)

    tw`bg-gray-200 ${false && 'rounded'}`
    
  • Objects

    tw({ 'bg-gray-200': true, rounded: false, underline: isTrue() })
    
  • Strings

    tw('bg-gray-200', true && 'rounded', 'underline')
    
  • Arrays

    tw(['bg-gray-200'], ['', 0, false, 'rounded'], [['underline']])
    
  • Variadic (mixed)

    tw('bg-gray-200', [
      1 && 'rounded',
      { underline: false, 'text-black': null },
      ['text-lg', ['shadow-lg']],
    ])
    

Falsy values, standalone booleans, and number values are always discarded:

// Strings
tw('bg-gray-200', true && 'rounded', 'underline')
//=> bg-gray-200 rounded underline

// Objects
tw({ 'bg-gray-200': true, rounded: false, underline: isTrue() })
//=> bg-gray-200 underline

// Arrays
tw(['bg-gray-200', 0, false, 'rounded'])
//=> bg-gray-200 rounded

// Mixed
tw('bg-gray-200', [
  1 && 'rounded',
  { underline: false, 'text-black': null },
  ['text-lg', ['shadow-lg']],
])
//=> bg-gray-200 rounded text-lg shadow-lg

The apply function

The apply function is used to compose styles that can be later be overwritten in a tw call. It's a companion to the tw function and useful for composition.

💡 apply accepts the same arguments as the tw function

WARNING

The apply function returns a function that must be passed to a tw call before those styles are applied to the document. In other words, the function does nothing outside of a tw call.

import { apply } from 'twind'

const btn = apply`inline-block bg-gray-500 text-base`
// => generates a CSS class with all declarations of the above rules when used

const btnBlock = apply`${btn} block`
// => generates a CSS class with all declarations of btn & block

<button class={tw`${btn}`}>gray-500</button>
// => tw-XXXXX

<button class={tw`${btn} bg-red-500 text-lg`}>red-500 large</button>
// => tw-XXXX bg-red-500 text-lg

<button class={tw`${btnBlock}`}>block button</button>
// => tw-YYYY

TIP

Another way to extract common component styles is by using plugins.

The apply function allows you to use Twind rules and utility classes to define your preflight styles:

setup({
  preflight: {
    body: apply('bg-gray-900 text-white'),
  },
})

The css function can be used with the apply function to define additional styles:

const btn = apply`
  py-2 px-4
  ${css({
    borderColor: 'black',
  })}
`

The apply function can be used within the css function:

const prose = css(
  apply`text-gray-700 dark:text-gray-300`,
  {
    p: apply`my-5`,
    h1: apply`text-black dark:text-white`,
  },
  {
    h1: {
      fontWeight: '800',
      fontSize: '2.25em',
      marginTop: '0',
      marginBottom: '0.8888889em',
      lineHeight: '1.1111111',
    },
  },
)

Using template literal syntax:

const prose = css`
  ${apply`text-gray-700 dark:text-gray-300`}

  p {
    ${apply`my-5`}
  }

  h1 {
    ${apply`text-black dark:text-white`}
    font-weight: 800;
    font-size: 2.25em;
    margin-top: 0;
    margin-bottom: 0.8888889em;
    line-height: 1.1111111;
  }
`

Using Tailwind directives with animation function from the twind/css module:

const motion = animation('.6s ease-in-out infinite', {
  '0%': apply`scale-100`,
  '50%': apply`scale-125 rotate-45`,
  '100%': apply`scale-100 rotate-0`,
})

const bounce = animation(
  '1s ease infinite',
  keyframes`
  from, 20%, 53%, 80%, to {
    ${apply`transform-gpu translate-x-0`}
  }
  40%, 43% {
    ${apply`transform-gpu -translate-x-7`}
  }
  70% {
    ${apply`transform-gpu -translate-x-3.5`}
  },
  90% {
    ${apply`transform-gpu -translate-x-1`}
  }
`,
)

A React button component

import { tw } from 'twind'

const variantMap = {
  success: 'green',
  primary: 'blue',
  warning: 'yellow',
  info: 'gray',
  danger: 'red',
}

const sizeMap = {
  sm: apply`text-xs py(2 md:1) px-2`,
  md: apply`text-sm py(3 md:2) px-2`,
  lg: apply`text-lg py-2 px-4`,
  xl: apply`text-xl py-3 px-6`,
}

const baseStyles = apply`
  w(full md:auto)
  text(sm white uppercase)
  px-4
  border-none
  transition-colors
  duration-300
`

function Button({
  size = 'md',
  variant = 'primary',
  round = false,
  disabled = false,
  className,
  children,
}) {
  // Collect all styles into one class
  const instanceStyles = apply`
    ${baseStyles}
    bg-${variantMap[variant]}(600 700(hover:& focus:&)))
    ${sizeMap[size]}
    rounded-${round ? 'full' : 'lg'}
    ${disabled && 'bg-gray-400 text-gray-100 cursor-not-allowed'}
  `

  // Allow passed classNames to override instance styles
  return <button className={tw(instanceStyles, className)}>{children}</button>
}

render(
  <Button variant="info" className="text-lg rounded-md">
    Click me
  </Button>,
)