Frontend Development 8 min read

How Tailwind CSS Uses PostCSS and Plugins to Generate Smart, Tree‑Shaken CSS

An in‑depth look at Tailwind CSS reveals how it harnesses PostCSS, JavaScript configuration, and custom plugins to generate and tree‑shake CSS, detailing classic @tailwind at‑rules, screen directives, and a sample z‑index plugin with code examples.

37 Mobile Game Tech Team
37 Mobile Game Tech Team
37 Mobile Game Tech Team
How Tailwind CSS Uses PostCSS and Plugins to Generate Smart, Tree‑Shaken CSS

What is Tailwind CSS

If you open Tailwind CSS's GitHub repository you might be confused by the mix of CSS and JavaScript files and the unusual at‑rules. Tailwind leverages PostCSS at a higher level: a JavaScript configuration is parsed by

postcss‑js

and

postcss‑nested

, producing a PostCSS AST that is then turned into CSS. Tools like PurgeCSS can further tree‑shake the generated CSS.

Classic Tailwind at‑rules

Three core at‑rules are used:

<code>@tailwind base;

@tailwind components;

@tailwind utilities;</code>

These at‑rules trigger PostCSS plugins to inject the corresponding CSS fragments and fix source maps.

<code>css.walkAtRules('tailwind', atRule => {
  if (atRule.params === 'preflight') {
    // prettier-ignore
    throw atRule.error("`@tailwind preflight` is not a valid at-rule in Tailwind v1.0, use `@tailwind base` instead.", { word: 'preflight' })
  }

  if (atRule.params === 'base') {
    atRule.before(updateSource(pluginBase, atRule.source))
    atRule.remove()
  }

  if (atRule.params === 'components') {
    atRule.before(postcss.comment({ text: 'tailwind start components' }))
    atRule.before(updateSource(pluginComponents, atRule.source))
    atRule.after(postcss.comment({ text: 'tailwind end components' }))
    atRule.remove()
  }

  if (atRule.params === 'utilities') {
    atRule.before(postcss.comment({ text: 'tailwind start utilities' }))
    atRule.before(updateSource(pluginUtilities, atRule.source))
    atRule.after(postcss.comment({ text: 'tailwind end utilities' }))
    atRule.remove()
  }

  if (atRule.params === 'screens') {
    includesScreensExplicitly = true
    atRule.before(postcss.comment({ text: 'tailwind start screens' }))
    atRule.after(postcss.comment({ text: 'tailwind end screens' }))
  }
})

if (!includesScreensExplicitly) {
  css.append([
    postcss.comment({ text: 'tailwind start screens' }),
    postcss.atRule({ name: 'tailwind', params: 'screens' }),
    postcss.comment({ text: 'tailwind end screens' }),
  ])
}</code>

The helper below ensures newly generated nodes keep the original source information for correct sourcemaps:

<code>function updateSource(nodes, source) {
  return _.tap(Array.isArray(nodes) ? postcss.root({ nodes }) : nodes, tree => {
    tree.walk(node => (node.source = source))
  })
}</code>

Screen at‑rule example

Instead of writing a raw media query that duplicates that value like this:
<code>@media (min-width: 640px) {
  /* ... */
}</code>
you can use the @screen directive and reference the breakpoint by name:
<code>@screen sm {
  /* ... */
}</code>

The directive reads the breakpoint values from

tailwind.config.js

(the

theme.screens

object) and generates the appropriate media query.

<code>import _ from 'lodash'
import buildMediaQuery from '../util/buildMediaQuery'

export default function({ theme }) {
  return function(css) {
    css.walkAtRules('screen', atRule => {
      const screen = atRule.params

      if (!_.has(theme.screens, screen)) {
        throw atRule.error(`No `${screen}` screen found.`)
      }

      atRule.name = 'media'
      atRule.params = buildMediaQuery(theme.screens[screen])
    })
  }
}</code>

Writing Tailwind CSS plugins

Plugins generate initial CSS from JavaScript configuration and then let PostCSS perform a second pass. Below is a simplified z‑index plugin example.

<code>import _ from 'lodash'
import prefixNegativeModifiers from '../util/prefixNegativeModifiers'

export default function() {
  return function({ addUtilities, e, theme, variants }) {
    const utilities = _.fromPairs(
      _.map(theme('zIndex'), (value, modifier) => {
        return [
          `.${e(prefixNegativeModifiers('z', modifier))}`,
          { 'z-index': value },
        ]
      })
    )

    addUtilities(utilities, variants('zIndex'))
  }
}</code>

The call

addUtilities(utilities, variants('zIndex'))

registers the generated utilities under

@tailwind utilities

. The

@variants responsive

wrapper then enables responsive variants, and a later PostCSS step (

substituteResponsiveAtRules

) finalizes the CSS.

<code>expect(addedUtilities).toEqual([
  {
    utilities: {
      '.-z-20': { 'z-index': '-20' },
      '.-z-10': { 'z-index': '-10' },
      '.z-10': { 'z-index': '10' },
      '.z-20': { 'z-index': '20' },
    },
    variants: ['responsive'],
  },
])</code>

Final note

Tailwind CSS offers many more tricks; the documentation is worth exploring. The most striking insight is how PostCSS can be used to treat CSS as a programmable language, opening up a huge space for creative tooling.

Frontend DevelopmentpluginsPostCSSTailwind CSSCSS Framework
37 Mobile Game Tech Team
Written by

37 Mobile Game Tech Team

37 Mobile Game Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.