An experimental just-in-time compiler for Tailwind CSS

  • By Tailwind Labs
  • Last update: Jan 4, 2023
  • Comments: 12

As of Tailwind CSS v2.1 this project has been merged with the core Tailwind CSS repository and all future development will happen there.


Tailwind CSS Just-In-Time

Latest Release License

Overview

An experimental just-in-time compiler for Tailwind CSS that generates your styles on-demand as you author your templates instead of generating everything in advance at initial build time.

This comes with a lot of advantages:

  • Lightning fast build times. Tailwind can take 3โ€“8s to initially compile using our CLI, and upwards of 30โ€“45s in webpack projects because webpack struggles with large CSS files. This library can compile even the biggest projects in about 800ms (with incremental rebuilds as fast as 3ms), no matter what build tool you're using.
  • Every variant is enabled out of the box. Variants like focus-visible, active, disabled, and others are not normally enabled by default due to file-size considerations. Since this library generates styles on demand, you can use any variant you want, whenever you want. You can even stack them like sm:hover:active:disabled:opacity-75. Never configure your variants again.
  • Generate arbitrary styles without writing custom CSS. Ever needed some ultra-specific value that wasn't part of your design system, like top: -113px for a quirky background image? Since styles are generated on demand, you can just generate a utility for this as needed using square bracket notation like top-[-113px]. Works with variants too, like md:top-[-113px].
  • Your CSS is identical in development and production. Since styles are generated as they are needed, you don't need to purge unused styles for production, which means you see the exact same CSS in all environments. Never worry about accidentally purging an important style in production again.
  • Better browser performance in development. Since development builds are as small as production builds, the browser doesn't have to parse and manage multiple megabytes of pre-generated CSS. In projects with heavily extended configurations this makes dev tools a lot more responsive.

To see it in action, watch our announcement video.

Getting started

Install @tailwindcss/jit from npm:

npm install -D @tailwindcss/jit tailwindcss postcss

The existing tailwindcss library is a peer-dependency of @tailwindcss/jit, and is also needed for compatibility with Tailwind plugins.

Add @tailwindcss/jit to your PostCSS configuration (instead of tailwindcss):

  // postcss.config.js
  module.exports = {
    plugins: {
      '@tailwindcss/jit': {},
      autoprefixer: {},
    }
  }

If you are using autoprefixer, make sure you are on the latest version using npm install -D autoprefixer@latest โ€” there's a bug in older versions that makes it incompatible with this library.

Configure the purge option in your tailwind.config.js file with all of your template paths:

// tailwind.config.js
module.exports = {
  purge: [
    './public/**/*.html',
    './src/**/*.{js,jsx,ts,tsx,vue}',
  ],
  theme: {
    // ...
  }
  // ...
}

Now start your dev server or build tool as you normally would and you're good to go.

Make sure you set NODE_ENV=development if you are running a watcher, or Tailwind won't watch your template files for changes. Set NODE_ENV=production for one-off builds.

If you want to control whether Tailwind watches files or not more explicitly, set TAILWIND_MODE=watch or TAILWIND_MODE=build to override the default NODE_ENV-based behavior.

For example if you want to do one-off builds with NODE_ENV=development, explicitly set TAILWIND_MODE=build so that Tailwind knows you are just doing a one-off build and doesn't hang.

Documentation

This library is simply a new internal engine for Tailwind CSS, so for a complete API reference visit the official Tailwind CSS documentation.

The on-demand nature of this new engine does afford some new features that weren't possible before, which you can learn about below.

All variants are enabled out of the box

Since styles are generated on-demand, there's no need to configure which variants are available for each core plugin.

">
<input class="disabled:opacity-75">

You can use variants like focus-visible, active, disabled, even, and more in combination with any utility, without making any changes to your tailwind.config.js file.

Stackable variants

All variants can be combined together to easily target very specific situations without writing custom CSS.

">
<button class="md:dark:disabled:focus:hover:bg-gray-400">

Arbitrary value support

Many utilities support arbitrary values using a new square bracket notation to indicate that you're "breaking out" of your design system.

">

<img class="absolute w-[762px] h-[918px] top-[-325px] right-[62px] md:top-[-400px] md:right-[80px]" src="/crazy-background-image.png">


<button class="bg-[#1da1f1]">Share on Twitterbutton>


<div class="grid-cols-[1fr,700px,2fr]">
  
div>

This is very useful for building pixel-perfect designs where there are a few elements that need hyper-specific styles, like a carefully positioned background image on a marketing site.

We'll likely add some form of "strict mode" in the future for power-hungry team leads who don't trust their colleagues to use this feature responsibly.

Built-in important modifier

You can make any utility important by adding a ! character to the beginning:

This will be medium even though bold comes later in the CSS.

">
<p class="font-bold !font-medium">
  This will be medium even though bold comes later in the CSS.
p>

The ! always goes at the beginning of the utility name, after any variants, but before any prefix:

- !sm:hover:tw-font-bold
+ sm:hover:!tw-font-bold

This can be useful in rare situations where you need to increase specificity because you're at war with some styles you don't control.

Known limitations

This library is very close to feature parity with tailwindcss currently and for most projects I bet you'll find it works exactly as you'd expect.

There are a few items still on our todo list though that we are actively working on:

  • Advanced PurgeCSS options like safelist aren't supported yet since we aren't actually using PurgeCSS. We'll add a way to safelist classes for sure though. For now, a safelist.txt file somewhere in your project with all the classes you want to safelist will work fine.
  • You can only @apply classes that are part of core, generated by plugins, or defined within a @layer rule. You can't @apply arbitrary CSS classes that aren't defined within a @layer rule.
  • Currently the JIT project only supports PostCSS 8. We may do a compat build like we do for Tailwind, but it isn't a priority right now.

If you run into any other issues or find any bugs, please open an issue so we can fix it.

Roadmap

Eventually we plan to merge this project with tailwindcss and expose it via an option in your tailwind.config.js file, something like this:

// tailwind.config.js
module.exports = {
  mode: 'jit',
  purge: [
    // ...
  ],
  theme: {
    // ...
  }
  // ...
}

Once it's been heavily tested by the community and we've worked out any kinks, we hope to make it the default mode for Tailwind CSS v3.0 later this year.

We'll always provide a mode: 'aot' option for people who want to generate the stylesheet in advance and purge later โ€” we'll need that ourselves for our CDN builds.

License

This library is MIT licensed.

Github

https://github.com/tailwindlabs/tailwindcss-jit

Comments(12)

  • 1

    Possible to run with Laravel Mix?

    Super pumped to try Tailwind JIT ๐Ÿคฉ

    I'm having trouble adding it to a Statamic project using Laravel Mix to configure PostCSS, but I'm not even sure if that's the problem or something entirely different.

    webpack.mix.js

    mix.postCss('resources/css/tailwind.css', 'public/css', [
        require('postcss-import'),
        require('@tailwindcss/jit')({}),
        require('postcss-nested'),
        require('postcss-preset-env')({stage: 0})
    ])
    

    Compilation throws the following error:

     Mix
      Compiled with some errors in 1.00s
    
    ERROR in ./resources/css/tailwind.css
    Module build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.js):
    ModuleBuildError: Module build failed (from ./node_modules/postcss-loader/dist/cjs.js):
    TypeError: Cannot read property 'split' of undefined
        at replaceSelector (/Users/daniel/www/statamic.schreinerei-neub/node_modules/@tailwindcss/jit/src/lib/expandApplyAtRules.js:84:12)
        at /Users/daniel/www/statamic.schreinerei-neub/node_modules/@tailwindcss/jit/src/lib/expandApplyAtRules.js:116:31
        at /Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss/lib/container.js:115:18
        at /Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss/lib/container.js:74:18
        at Root.each (/Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss/lib/container.js:60:16)
        at Root.walk (/Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss/lib/container.js:71:17)
        at Root.walkRules (/Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss/lib/container.js:113:19)
        at /Users/daniel/www/statamic.schreinerei-neub/node_modules/@tailwindcss/jit/src/lib/expandApplyAtRules.js:115:18
        at LazyResult.runOnRoot (/Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss/lib/lazy-result.js:303:16)
        at LazyResult.runAsync (/Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss/lib/lazy-result.js:355:26)
        at processResult (/Users/daniel/www/statamic.schreinerei-neub/node_modules/webpack/lib/NormalModule.js:598:19)
        at /Users/daniel/www/statamic.schreinerei-neub/node_modules/webpack/lib/NormalModule.js:692:5
        at /Users/daniel/www/statamic.schreinerei-neub/node_modules/loader-runner/lib/LoaderRunner.js:399:11
        at /Users/daniel/www/statamic.schreinerei-neub/node_modules/loader-runner/lib/LoaderRunner.js:251:18
        at context.callback (/Users/daniel/www/statamic.schreinerei-neub/node_modules/loader-runner/lib/LoaderRunner.js:124:13)
        at Object.loader (/Users/daniel/www/statamic.schreinerei-neub/node_modules/postcss-loader/dist/index.js:104:7)
    
    1 ERROR in child compilations (Use 'stats.children: true' resp. '--stats-children' for more details)
    webpack compiled with 2 errors
    
    
  • 2

    Next.js: Error: Cannot find module '@tailwindcss/jit'

    I'm running Next.js in a Docker container mounted to my project directory. I get the message: ready - started server on 0.0.0.0:3000, url: http://localhost:3000, but I get the error Error: Cannot find module '@tailwindcss/jit' right after. The latest versions of @tailwindcss/jit, tailwindcss, postcss, and autoprefixer are installed as dependencies. PostCSS config:

    module.exports = {
      plugins: {
        '@tailwindcss/jit': {},
        autoprefixer: {},
      },
    };
    
    Full Error
    Error: Cannot find module '@tailwindcss/jit'
    web_1  | Require stack:
    web_1  | - /usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/plugins.js
    web_1  | - /usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/index.js
    web_1  | - /usr/src/app/node_modules/next/dist/build/webpack/config/index.js
    web_1  | - /usr/src/app/node_modules/next/dist/build/webpack-config.js
    web_1  | - /usr/src/app/node_modules/next/dist/server/hot-reloader.js
    web_1  | - /usr/src/app/node_modules/next/dist/server/next-dev-server.js
    web_1  | - /usr/src/app/node_modules/next/dist/server/next.js
    web_1  | - /usr/src/app/node_modules/next/dist/server/lib/start-server.js
    web_1  | - /usr/src/app/node_modules/next/dist/cli/next-dev.js
    web_1  | - /usr/src/app/node_modules/next/dist/bin/next
    web_1  |     at Function.Module._resolveFilename (node:internal/modules/cjs/loader:924:15)
    web_1  |     at Function.mod._resolveFilename (/usr/src/app/node_modules/next/dist/build/webpack/require-hook.js:4:1784)
    web_1  |     at Function.resolve (node:internal/modules/cjs/helpers:98:19)
    web_1  |     at loadPlugin (/usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/plugins.js:1:1556)
    web_1  |     at /usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/plugins.js:7:1761      
    web_1  |     at Array.map (<anonymous>)
    web_1  |     at getPostCssPlugins (/usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/plugins.js:7:1754)
    web_1  |     at async __overrideCssConfiguration (/usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/overrideCssConfiguration.js:1:272)
    web_1  |     at async getBaseWebpackConfig (/usr/src/app/node_modules/next/dist/build/webpack-config.js:144:166)
    web_1  |     at async Promise.all (index 0)
    web_1  |     at async HotReloader.start (/usr/src/app/node_modules/next/dist/server/hot-reloader.js:14:1997)
    web_1  |     at async DevServer.prepare (/usr/src/app/node_modules/next/dist/server/next-dev-server.js:15:414)
    web_1  |     at async /usr/src/app/node_modules/next/dist/cli/next-dev.js:22:1 {
    web_1  |   code: 'MODULE_NOT_FOUND',
    web_1  |   requireStack: [
    web_1  |     '/usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/plugins.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/build/webpack/config/blocks/css/index.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/build/webpack/config/index.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/build/webpack-config.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/server/hot-reloader.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/server/next-dev-server.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/server/next.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/server/lib/start-server.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/cli/next-dev.js',
    web_1  |     '/usr/src/app/node_modules/next/dist/bin/next'
    
  • 3

    Next.js with webpack 5 hangs on first compile (.next folder is empty)

    Hey there, using a brand new create-next-app, using webpack 5, all dependencies up to date, following the tailwindcss-jit README then on first load (when .next folder is empty) the build never finishes.

    You have to CTRL+C and then start again for it to work.

    Example repo: https://github.com/vvo/tailwindcss-jit-hangs-webpack5-next.js

    git clone [email protected]:vvo/tailwindcss-jit-hangs-webpack5-next.js.git
    cd tailwindcss-jit-hangs-webpack5-next
    yarn && yarn dev
    

    Now try to load http://localhost:3000 and it will hang forever. If you restart the server it will work.

    To reproduce the hang, rm -rf .next.

    Disabling webpack 5 solves this issue, using webpack 5 using regular tailwindcss works.

    PS: On my real-world project, the build never finishes at all, first load or second load, I could not reproduce this behavior though ๐Ÿคท

  • 4

    curious: what about applying classes in devtools?

    First of all excellent effort! :tada: only I know how many times my PC hanged/stopped working when working in devtools :fire: . JIT is blazing fast :rocket: .

    Now the new issue is, I miss the ability to apply classes on the fly in devtools. Was wondering if you guys having something related to this in your genius minds?

    For now I'm just anticipating & adding a comment similar to this /* overflow-hidden overflow-auto */ to generate the classes temporarily. It's tedious.

    Maybe a special comment syntax like this /* tailwind-generate flex overflow[responsive] */ to generate all flex & overflow responsive related classes temporarily would be nice. I don't know. what's your plan?

  • 5

    Flicker on updates with Vite

    First, thanks a lot for the awesome tailwindcss-jit. It's so awesome to have this incredible speed :)

    I've noticed that with Vite when making updates in any file that adds a class that is not cached, it will flicker first without the new class and then will update with the used class. Is this beacuse Vite is too fast? :D Could this be resolved with a Vite plugin that adds tailwindcss-jit as part of HMR, so it uses the watch process of Vite and doesn't do the update in two steps maybe?

    Thanks!!

  • 6

    PostCSS 7 Compatibility build

    Is there a chance we could get a PostCSS 7 Compatibility build? I would love to try it out, but my Vue 2 application doesn't support Post CSS 8.

    Thank you for your awesome work!

  • 7

    Svelte's conditional class syntax is not picked up correctly

    First of all, thank you for the great work on Tailwind and JIT in particular, I'm really enjoying the new DX.

    I tried to put together a new side project using Svelte + Vite + Tailwind and I couldn't get the JIT to recognize Svelte's conditional class syntax:

    <h1 class:text-blue-500={shouldBeBlue}>Hello, World</h1>
    

    Here's a fairly minimal repro: https://github.com/Gustorn/tailwind-svelte-repro

  • 8

    JIT compilation doesn't remove unused classes when the DOM changes

    If I was to write the following snippet with the (Laravel Mix) JIT-compiler watching:

    <div class="relative max-w-md bg-white rounded shadow"></div>
    

    Then decide I want to have shadow-md instead:

    - <div class="relative max-w-md bg-white rounded shadow"></div>
    + <div class="relative max-w-md bg-white rounded shadow-md"></div>
    

    The compiled stylesheet will have both .shadow and .shadow-md in it.

    .shadow {
            --tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
            box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
    }
    .shadow-md {
            --tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
            box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
    }
    

    If I restart the watcher, .shadow is dropped from the compiled stylesheet.

  • 9

    Hot module replacement compatibility

    Hi there,

    I've switched to JIT module for a project to test it out and the project is using webpack encore. Does this support HMR? It works in npm run watch mode, but in HMR it only gets the initial styles but any change is not picked up, I have to restart the server to see changes.

  • 10

    css-loader can't handle paths with spaces

    Hey!

    If the project is in a folder that has a space in its name anywhere along the path, css-loader breaks, but without spaces it works (that should teach me for using spaces, I know ๐Ÿ˜…):

    Error: ENOENT: no such file or directory, stat '/Users/home/Some%20Projects/a-project/src/styles/index.css'
    # or
    Error: ENOENT: no such file or directory, stat '/Users/home/Some%20Projects/a-project/tailwind.config.js'
    

    Using tailwindcss in postcss.config.js also works properly, with and without spaces.

    The same thing happens in any kind of project (reproduced it in Next.js, and in razzle.js).

  • 11

    Maximum call stack size exceeded

    What version of @tailwindcss/jit are you using?

    v0.1.11 โ†’ (Updated: v0.1.12)

    What version of Node.js are you using?

    v14.15.1

    What browser are you using?

    Safari

    What operating system are you using?

    macOS

    Reproduction repository

    https://github.com/edgarasben/bulletproof-ui/blob/main/src/Button.jsx

    I get an error only when I switch to '@tailwindcss/jit' PostCSS plugin and I am trying to build a conditional CSS classes with React + Vite.

    The error occurs when I add this to the Button.jsx file:

      const types = {
        primary: 'bg-blue-600 hover:bg-blue-700 text-white',
        secondary:
          'bg-blue-700 text-blue-600 hover:bg-blue-700 bg-opacity-20 hover:bg-opacity-30',
      }
      const whichType = types[props.buttonType]
    

    Error log

    [plugin:vite:css] Maximum call stack size exceeded
    /Users/edgaras/Documents/Sites/bulletproof-ui/src/index.css
        at candidatePermutations (/Users/edgaras/Documents/Sites/bulletproof-ui/node_modules/@tailwindcss/jit/src/lib/generateRules.js:19:32)
        at candidatePermutations (/Users/edgaras/Documents/Sites/bulletproof-ui/node_modules/@tailwindcss/jit/src/lib/generateRules.js:41:10)
        at candidatePermutations.next (<anonymous>)
        at candidatePermutations (/Users/edgaras/Documents/Sites/bulletproof-ui/node_modules/@tailwindcss/jit/src/lib/generateRules.js:41:10)
        at candidatePermutations.next (<anonymous>)
        at candidatePermutations (/Users/edgaras/Documents/Sites/bulletproof-ui/node_modules/@tailwindcss/jit/src/lib/generateRules.js:41:10)
        at candidatePermutations.next (<anonymous>)
        at candidatePermutations (/Users/edgaras/Documents/Sites/bulletproof-ui/node_modules/@tailwindcss/jit/src/lib/generateRules.js:41:10)
        at candidatePermutations.next (<anonymous>)
        at candidatePermutations (/Users/edgaras/Documents/Sites/bulletproof-ui/node_modules/@tailwindcss/jit/src/lib/generateRules.js:41:10
    
  • 12

    Add support for wildcard utility matching

    This is a work in progress. I'd like to see if this is a reasonable approach or if there are better ideas.

    My first thought was to turn candidates (conditionally) into RegExp objects and match the utilities against them. That would be pretty powerful. Unfortunately the idea that a candidate is a string is pretty pervasive and also core to how matchUtilities, modifiers, and what not all work. For them to be regular expressions we'd have to generate all the classes in the tailwind config up front in memory and then match against the generated list.

    My second thought was to add support for wildcards in asValue() but that would require supporting multiple return values โ€” which for a prototype would be a significant change across all plugins just to not break something.

    So end the end I settled with giving plugins the ability to explicitly register wildcard support. When you call a plugin you can say matchWildcards({ px: ['0', '1', '2', '3', โ€ฆ], โ€ฆ}) and when it finds p-{*} when resolving matches it expands to p-1, p-2, p-3, p-4, etcโ€ฆ

    Due to implementation it includes duplicate utilities if you do p-{*} and p-1 / p-2 / etcโ€ฆ I need to find a way to clean this up / fix it but I wanted to get the reactions to the general approach first.

    I've added wildcard support to the padding plugin only in this draft.