Installing TailwindCSS in Rails 6
This post is a detailed walkthrough showing how to install TailwindCSS in Rails 6.
Our goal is a page styled with TailwindCSS via Rails:

Who is this for?
This walkthrough assumes basic Rails and command line chops and a working Rails 6 installation on your development environment. Otherwise it's 100% start to finish, so it's appropriate for anyone looking to dip their toes in TailwindCSS on Rails. That said, this is a complex configuration—not for the faint of heart.
I personally struggled with this setup several times. I figured I'd document the approach that worked best for me (for my own reference and so others can try it out). Below, you'll find detailed commands, code, and outputs.
I go back and forth on whether Webpacker's juice is worth its squeeze. In most cases, I find it easier to write my own JS and CSS or simply reach for a no-code tool. I'm not a huge fan of swimming in complicated waters unless it's completely necessary. When it is, TailwindCSS on Rails is a powerful toolset.
In any event, this walkthrough demonstrates how to set things up so you can test it out and decide for yourself.
Table of Contents - Installing TailwindCSS on Rails 6
A. Create a Rails 6 App ↑ Table of Contents
$ rails -v
Rails 6.1.3
Replace app_name
with whatever you want to name your app.
$ rails new app_name -d postgresql[output omitted]
$ cd app_name
$ rails db:create
$ rails s[output omitted]
localhost:3000
to make sure your app was installed successfully:
B. Webpacker / PostCSS 8 Setup ↑ Table of Contents
Next, we'll machete through some dense JS config.
There's a versioning hiccup that prevents Webpacker 5.0 (the Rails gem that powers Webpack in Rails) from working with PostCSS 8 (the JS package that allows you to write CSS using JavaScript).
The following workaround comes from David Teren's tutorial.
$ yarn remove @rails/webpacker
yarn remove v1.22.10[1/2] 🗑 Removing module @rails/webpacker...[2/2] 🔨 Regenerating lockfile and installing missing dependencies...success Uninstalled packages.
✨ Done in 3.65s.
I'm including the long, full output of this command so we can see what's happening under the hood. For the record, this complexity really turns me off to all things Webpacker/Webpack. But let's just roll with it.
$ yarn add 'rails/webpacker#b6c2180'
yarn add v1.22.10[1/4] 🔍 Resolving packages...[2/4] 🚚 Fetching packages...[3/4] 🔗 Linking dependencies...[4/4] 🔨 Building fresh packages...success Saved lockfile.success Saved 391 new dependencies.info Direct dependencies
└─ @rails/webpacker@5.1.1info All dependencies
├─ @babel/code-frame@7.12.13
├─ @babel/core@7.13.10
├─ @babel/generator@7.13.9
├─ @babel/helper-builder-binary-assignment-operator-visitor@7.12.13
├─ @babel/helper-compilation-targets@7.13.10
├─ @babel/helper-explode-assignable-expression@7.13.0
├─ @babel/helper-get-function-arity@7.12.13
├─ @babel/helper-hoist-variables@7.13.0
├─ @babel/helper-replace-supers@7.13.0
├─ @babel/helper-wrap-function@7.13.0
├─ @babel/helpers@7.13.10
├─ @babel/highlight@7.13.10
├─ @babel/parser@7.13.11
├─ @babel/plugin-proposal-async-generator-functions@7.13.8
├─ @babel/plugin-proposal-class-properties@7.13.0
├─ @babel/plugin-proposal-dynamic-import@7.13.8
├─ @babel/plugin-proposal-export-namespace-from@7.12.13
├─ @babel/plugin-proposal-json-strings@7.13.8
├─ @babel/plugin-proposal-logical-assignment-operators@7.13.8
├─ @babel/plugin-proposal-nullish-coalescing-operator@7.13.8
├─ @babel/plugin-proposal-numeric-separator@7.12.13
├─ @babel/plugin-proposal-object-rest-spread@7.13.8
├─ @babel/plugin-proposal-optional-catch-binding@7.13.8
├─ @babel/plugin-proposal-optional-chaining@7.13.8
├─ @babel/plugin-proposal-private-methods@7.13.0
├─ @babel/plugin-proposal-unicode-property-regex@7.12.13
├─ @babel/plugin-syntax-class-properties@7.12.13
├─ @babel/plugin-syntax-top-level-await@7.12.13
├─ @babel/plugin-transform-arrow-functions@7.13.0
├─ @babel/plugin-transform-async-to-generator@7.13.0
├─ @babel/plugin-transform-block-scoped-functions@7.12.13
├─ @babel/plugin-transform-block-scoping@7.12.13
├─ @babel/plugin-transform-classes@7.13.0
├─ @babel/plugin-transform-computed-properties@7.13.0
├─ @babel/plugin-transform-destructuring@7.13.0
├─ @babel/plugin-transform-dotall-regex@7.12.13
├─ @babel/plugin-transform-duplicate-keys@7.12.13
├─ @babel/plugin-transform-exponentiation-operator@7.12.13
├─ @babel/plugin-transform-for-of@7.13.0
├─ @babel/plugin-transform-function-name@7.12.13
├─ @babel/plugin-transform-literals@7.12.13
├─ @babel/plugin-transform-member-expression-literals@7.12.13
├─ @babel/plugin-transform-modules-amd@7.13.0
├─ @babel/plugin-transform-modules-commonjs@7.13.8
├─ @babel/plugin-transform-modules-systemjs@7.13.8
├─ @babel/plugin-transform-modules-umd@7.13.0
├─ @babel/plugin-transform-named-capturing-groups-regex@7.12.13
├─ @babel/plugin-transform-new-target@7.12.13
├─ @babel/plugin-transform-object-super@7.12.13
├─ @babel/plugin-transform-property-literals@7.12.13
├─ @babel/plugin-transform-regenerator@7.12.13
├─ @babel/plugin-transform-reserved-words@7.12.13
├─ @babel/plugin-transform-runtime@7.13.10
├─ @babel/plugin-transform-shorthand-properties@7.12.13
├─ @babel/plugin-transform-spread@7.13.0
├─ @babel/plugin-transform-sticky-regex@7.12.13
├─ @babel/plugin-transform-template-literals@7.13.0
├─ @babel/plugin-transform-typeof-symbol@7.12.13
├─ @babel/plugin-transform-unicode-escapes@7.12.13
├─ @babel/plugin-transform-unicode-regex@7.12.13
├─ @babel/preset-env@7.13.10
├─ @babel/preset-modules@0.1.4
├─ @babel/runtime@7.13.10
├─ @discoveryjs/json-ext@0.5.2
├─ @npmcli/move-file@1.1.2
├─ @rails/webpacker@5.1.1
├─ @types/json-schema@7.0.7
├─ @types/parse-json@4.0.0
├─ @types/q@1.5.4
├─ @webassemblyjs/floating-point-hex-parser@1.9.0
├─ @webassemblyjs/helper-code-frame@1.9.0
├─ @webassemblyjs/helper-fsm@1.9.0
├─ @webassemblyjs/helper-wasm-section@1.9.0
├─ @webassemblyjs/wasm-edit@1.9.0
├─ @webassemblyjs/wasm-opt@1.9.0
├─ @webpack-cli/configtest@1.0.1
├─ @webpack-cli/info@1.2.2
├─ @webpack-cli/serve@1.3.0
├─ @xtuc/ieee754@1.2.0
├─ acorn@6.4.2
├─ aggregate-error@3.1.0
├─ argparse@1.0.10
├─ asn1.js@5.4.1
├─ assert@1.5.0
├─ autoprefixer@9.8.6
├─ babel-loader@8.2.2
├─ babel-plugin-macros@3.0.1
├─ base64-js@1.5.1
├─ bluebird@3.7.2
├─ boolbase@1.0.0
├─ brorand@1.1.0
├─ browserify-aes@1.2.0
├─ browserify-cipher@1.0.1
├─ browserify-des@1.0.2
├─ browserify-rsa@4.1.0
├─ browserify-sign@4.2.1
├─ browserify-zlib@0.2.0
├─ buffer-xor@1.0.3
├─ buffer@4.9.2
├─ builtin-status-codes@3.0.0
├─ caller-callsite@2.0.0
├─ caller-path@2.0.0
├─ callsites@3.1.0
├─ camelcase@6.2.0
├─ caniuse-lite@1.0.30001203
├─ case-sensitive-paths-webpack-plugin@2.4.0
├─ chrome-trace-event@1.0.2
├─ cipher-base@1.0.4
├─ clean-stack@2.2.0
├─ clone-deep@4.0.1
├─ coa@2.0.2
├─ color-string@1.5.5
├─ color@3.1.3
├─ compression-webpack-plugin@6.1.1
├─ concat-stream@1.6.2
├─ console-browserify@1.2.0
├─ constants-browserify@1.0.0
├─ convert-source-map@1.7.0
├─ copy-concurrently@1.0.5
├─ core-js-compat@3.9.1
├─ core-js@3.9.1
├─ create-ecdh@4.0.4
├─ create-hmac@1.1.7
├─ cross-spawn@7.0.3
├─ crypto-browserify@3.12.0
├─ css-blank-pseudo@0.1.4
├─ css-color-names@0.0.4
├─ css-declaration-sorter@4.0.1
├─ css-has-pseudo@0.10.0
├─ css-loader@5.1.3
├─ css-prefers-color-scheme@3.1.1
├─ css-select-base-adapter@0.1.1
├─ css-select@2.1.0
├─ css-tree@1.0.0-alpha.37
├─ css-what@3.4.2
├─ cssdb@4.4.0
├─ cssnano-preset-default@4.0.7
├─ cssnano-util-raw-cache@4.0.1
├─ cssnano-util-same-parent@4.0.1
├─ cssnano@4.1.10
├─ csso@4.2.0
├─ cyclist@1.0.1
├─ des.js@1.0.1
├─ diffie-hellman@5.0.3
├─ dom-serializer@0.2.2
├─ domain-browser@1.2.0
├─ domelementtype@1.3.1
├─ domutils@1.7.0
├─ dot-prop@5.3.0
├─ duplexify@3.7.1
├─ electron-to-chromium@1.3.693
├─ enhanced-resolve@4.5.0
├─ enquirer@2.3.6
├─ entities@2.2.0
├─ envinfo@7.7.4
├─ es-to-primitive@1.2.1
├─ escalade@3.1.1
├─ escape-string-regexp@1.0.5
├─ eslint-scope@4.0.3
├─ esprima@4.0.1
├─ esrecurse@4.3.0
├─ estraverse@4.3.0
├─ esutils@2.0.3
├─ events@3.3.0
├─ evp_bytestokey@1.0.3
├─ execa@5.0.0
├─ fastest-levenshtein@1.0.12
├─ file-loader@6.2.0
├─ flatted@3.1.1
├─ flatten@1.0.3
├─ flush-write-stream@1.1.1
├─ from2@2.3.0
├─ gensync@1.0.0-beta.2
├─ get-stream@6.0.0
├─ has-bigints@1.0.1
├─ hash.js@1.1.7
├─ hex-color-regex@1.1.0
├─ hmac-drbg@1.0.1
├─ hsl-regex@1.0.0
├─ hsla-regex@1.0.0
├─ html-comment-regex@1.1.2
├─ https-browserify@1.0.0
├─ human-signals@2.1.0
├─ ieee754@1.2.1
├─ import-fresh@3.3.0
├─ indent-string@4.0.0
├─ infer-owner@1.0.4
├─ interpret@2.2.0
├─ is-arrayish@0.2.1
├─ is-bigint@1.0.1
├─ is-boolean-object@1.1.0
├─ is-callable@1.2.3
├─ is-color-stop@1.1.0
├─ is-core-module@2.2.0
├─ is-directory@0.3.1
├─ is-negative-zero@2.0.1
├─ is-number-object@1.0.4
├─ is-obj@2.0.0
├─ is-resolvable@1.1.0
├─ is-stream@2.0.0
├─ is-svg@3.0.0
├─ is-symbol@1.0.3
├─ jest-worker@26.6.2
├─ js-tokens@4.0.0
├─ jsesc@2.5.2
├─ json-parse-better-errors@1.0.2
├─ json-parse-even-better-errors@2.3.1
├─ last-call-webpack-plugin@3.0.0
├─ lines-and-columns@1.1.6
├─ loader-runner@2.4.0
├─ lodash.debounce@4.0.8
├─ lodash.get@4.4.2
├─ lodash.has@4.5.2
├─ lodash.memoize@4.1.2
├─ lodash.template@4.5.0
├─ lodash.templatesettings@4.2.0
├─ lodash.uniq@4.5.0
├─ make-dir@3.1.0
├─ mdn-data@2.0.4
├─ miller-rabin@4.0.1
├─ mimic-fn@2.1.0
├─ mini-css-extract-plugin@1.3.9
├─ minipass-collect@1.0.2
├─ minipass-flush@1.0.5
├─ minipass-pipeline@1.2.4
├─ minizlib@2.1.2
├─ mississippi@3.0.0
├─ move-concurrently@1.0.1
├─ nanoid@3.1.22
├─ neo-async@2.6.2
├─ node-libs-browser@2.2.1
├─ node-releases@1.1.71
├─ normalize-range@0.1.2
├─ normalize-url@3.3.0
├─ npm-run-path@4.0.1
├─ nth-check@1.0.2
├─ num2fraction@1.2.2
├─ object-inspect@1.9.0
├─ object.assign@4.1.2
├─ object.getownpropertydescriptors@2.1.2
├─ object.values@1.1.3
├─ onetime@5.1.2
├─ optimize-css-assets-webpack-plugin@5.0.4
├─ os-browserify@0.3.0
├─ p-map@4.0.0
├─ pako@1.0.11
├─ parallel-transform@1.2.0
├─ parent-module@1.0.1
├─ parse-asn1@5.1.6
├─ parse-json@5.2.0
├─ path-browserify@0.0.1
├─ path-complete-extname@1.0.0
├─ path-parse@1.0.6
├─ path-type@4.0.0
├─ picomatch@2.2.2
├─ pnp-webpack-plugin@1.6.4
├─ postcss-attribute-case-insensitive@4.0.2
├─ postcss-calc@7.0.5
├─ postcss-color-functional-notation@2.0.1
├─ postcss-color-gray@5.0.0
├─ postcss-color-hex-alpha@5.0.3
├─ postcss-color-mod-function@3.0.3
├─ postcss-color-rebeccapurple@4.0.1
├─ postcss-colormin@4.0.3
├─ postcss-convert-values@4.0.1
├─ postcss-custom-media@7.0.8
├─ postcss-custom-properties@8.0.11
├─ postcss-custom-selectors@5.1.2
├─ postcss-dir-pseudo-class@5.0.0
├─ postcss-discard-comments@4.0.2
├─ postcss-discard-duplicates@4.0.2
├─ postcss-discard-empty@4.0.1
├─ postcss-discard-overridden@4.0.1
├─ postcss-double-position-gradients@1.0.0
├─ postcss-env-function@2.0.2
├─ postcss-focus-visible@4.0.0
├─ postcss-focus-within@3.0.0
├─ postcss-font-variant@4.0.1
├─ postcss-gap-properties@2.0.0
├─ postcss-image-set-function@3.0.1
├─ postcss-initial@3.0.2
├─ postcss-lab-function@2.0.1
├─ postcss-logical@3.0.0
├─ postcss-media-minmax@4.0.0
├─ postcss-merge-longhand@4.0.11
├─ postcss-merge-rules@4.0.3
├─ postcss-minify-font-values@4.0.2
├─ postcss-minify-gradients@4.0.2
├─ postcss-minify-params@4.0.2
├─ postcss-minify-selectors@4.0.2
├─ postcss-nesting@7.0.1
├─ postcss-normalize-charset@4.0.1
├─ postcss-normalize-display-values@4.0.2
├─ postcss-normalize-positions@4.0.2
├─ postcss-normalize-repeat-style@4.0.2
├─ postcss-normalize-string@4.0.2
├─ postcss-normalize-timing-functions@4.0.2
├─ postcss-normalize-unicode@4.0.1
├─ postcss-normalize-url@4.0.1
├─ postcss-normalize-whitespace@4.0.2
├─ postcss-ordered-values@4.1.2
├─ postcss-overflow-shorthand@2.0.0
├─ postcss-page-break@2.0.0
├─ postcss-place@4.0.1
├─ postcss-preset-env@6.7.0
├─ postcss-pseudo-class-any-link@6.0.0
├─ postcss-reduce-initial@4.0.3
├─ postcss-reduce-transforms@4.0.2
├─ postcss-replace-overflow-wrap@3.0.0
├─ postcss-safe-parser@5.0.2
├─ postcss-selector-matches@4.0.0
├─ postcss-selector-not@4.0.1
├─ postcss-svgo@4.0.2
├─ postcss-unique-selectors@4.0.1
├─ process@0.11.10
├─ public-encrypt@4.0.3
├─ pumpify@1.5.1
├─ q@1.5.1
├─ querystring-es3@0.2.1
├─ randomfill@1.0.4
├─ read-cache@1.0.0
├─ rechoir@0.7.0
├─ regenerate-unicode-properties@8.2.0
├─ regenerator-runtime@0.13.7
├─ regenerator-transform@0.14.5
├─ regexpu-core@4.7.1
├─ regjsgen@0.5.2
├─ regjsparser@0.6.7
├─ resolve@1.20.0
├─ rgb-regex@1.0.1
├─ rgba-regex@1.0.0
├─ run-queue@1.0.3
├─ sass-loader@10.1.1
├─ sass@1.32.8
├─ sax@1.2.4
├─ setimmediate@1.0.5
├─ shallow-clone@3.0.1
├─ shebang-command@2.0.0
├─ shebang-regex@3.0.0
├─ simple-swizzle@0.2.2
├─ source-list-map@2.0.1
├─ source-map-support@0.5.19
├─ sprintf-js@1.0.3
├─ ssri@8.0.1
├─ stable@0.1.8
├─ stream-browserify@2.0.2
├─ stream-each@1.2.3
├─ stream-http@2.8.3
├─ string.prototype.trimend@1.0.4
├─ string.prototype.trimstart@1.0.4
├─ strip-final-newline@2.0.0
├─ style-loader@2.0.0
├─ stylehacks@4.0.3
├─ svgo@1.3.2
├─ tar@6.1.0
├─ terser-webpack-plugin@4.2.3
├─ terser@5.6.1
├─ through2@2.0.5
├─ timers-browserify@2.0.12
├─ timsort@0.3.0
├─ to-arraybuffer@1.0.1
├─ to-fast-properties@2.0.0
├─ ts-pnp@1.2.0
├─ tslib@1.14.1
├─ tty-browserify@0.0.0
├─ typedarray@0.0.6
├─ unbox-primitive@1.0.0
├─ unicode-canonical-property-names-ecmascript@1.0.4
├─ unicode-match-property-ecmascript@1.0.4
├─ unicode-match-property-value-ecmascript@1.2.0
├─ unicode-property-aliases-ecmascript@1.1.0
├─ unique-slug@2.0.2
├─ unquote@1.1.1
├─ util.promisify@1.0.1
├─ util@0.11.1
├─ v8-compile-cache@2.3.0
├─ vendors@1.0.4
├─ vm-browserify@1.1.2
├─ watchpack-chokidar2@2.0.1
├─ watchpack@1.7.5
├─ webpack-assets-manifest@3.1.1
├─ webpack-cli@4.5.0
├─ webpack-merge@5.7.3
├─ webpack@4.46.0
├─ which-boxed-primitive@1.0.2
├─ which@2.0.2
├─ wildcard@2.0.0
├─ worker-farm@1.7.0
├─ xtend@4.0.2
├─ yaml@1.10.2
└─ yocto-queue@0.1.0
✨ Done in 12.51s.
Gemfile
to use the new version of Webpacker:Remove gem 'webpacker', '~> 5.0'
Add gem "webpacker", github: "rails/webpacker", ref: 'b6c2180'
The relevant lines in the Gemfile should look like this:
bundle
to implement the changes:$ bundle
C. Install TailwindCSS ↑ Table of Contents
Next we'll install TailwindCSS, PostCSS, and required packages and plugins.
If you're not familiar with PostCSS, I recommend reading about it here. Writing CSS with JavaScript feels like straying off the normally-so-simple-and-fun Rails path a bit. But TailwindCSS is such a cool CSS frontend framework as is totally worth it for the right use cases.
$ yarn add tailwindcss postcss autoprefixer @tailwindcss/forms @tailwindcss/typography @tailwindcss/aspect-ratio[output omitted]
application.scss
file and require it in our application's compiled SCSS:Create a directory named app/javascript/stylesheets
and within that directory create an empty file named app/javascript/stylesheets/application.scss
.
In app/javascript/packs/application.js
, add a line of code that reads require("stylesheets/application.scss")
.
The application.js
file should look like this:
When we installed PostCSS, it created a config file named postcss.config.js
. We need to tell that config file that we're using TailwindCSS.
In postcss.config.js
add a line that reads require('tailwindcss')
. The file should look like this (with our added code at line 3):
Now PostCSS knows how to find TailwindCSS so it can work its mojo.
D. Configure Rails to Use TailwindCSS ↑ Table of Contents
Add the following two lines of code in app/views/layouts/application.html.erb
:
The file should look like this (with our added code at lines 8 and 11):
Now Rails knows to pull in the PostCSS/TailwindCSS setup from the previous section.
E. Configure TailwindCSS ↑ Table of Contents
Our final step is to configure TailwindCSS.
$ npx tailwindcss init --full
tailwindcss 2.0.4
✅ Created Tailwind config file: tailwind.config.js
This command creates a TailwindCSS configuration file at tailwindcss.config.js
. We'll use this file in the following steps to configure our plugins and settings.
tailwind.config.js
:Add the forms
, typography
, and aspect-ratio
plugins to TailwindCSS by adding the following three lines to tailwind.config.js
. These lines will go in the plugins
section toward the end of the config file (see full file below).
tailwind.config.js
:We included the Inter font in app/views/layouts/application.html.erb
file. But we still need to tell TailwindCSS where to find it. To do that, simply add the Inter font to tailwind.config.js
in the fontFamily
sans
section at line 167.
The full tailwind.config.js
file should look like this (with the added lines at 167 and 857-859). I'm including the full config file to get a sense of how much configuration goes into TailwindCSS.
This is another example of really heavy configuration and "CSS in JS" that turns me off to using TailwindCSS. On the other hand, there's definitely power and convenience in having all this configuration in one place, as opposed to scattered across CSS files.
F. Test the Installation ↑ Table of Contents
At this point, we should be good to go.
Let's test the install by firing up a development server and trying out some TailwindCSS classes in our HTML.
Pages
controller with an index
view:$ rails g controller Pages index
Running via Spring preloader in process 88532
create app/controllers/pages_controller.rb
route get 'pages/index'
invoke erb
create app/views/pages
create app/views/pages/index.html.erb
invoke test_unit
create test/controllers/pages_controller_test.rb
invoke helper
create app/helpers/pages_helper.rb
invoke test_unit
invoke assets
invoke scss
create app/assets/stylesheets/pages.scss
To test out Tailwind, we're going to lift some code from Tailwind Play and use it in our pages#index
view.
Replace the contents of app/views/pages/index.html.erb
with this:
This is the heart of TailwindCSS. Similar to Bootstrap, we can add CSS classes to draw on Tailwind's extensive CSS library. For a technical explanation of why this is an efficient and scalable approach, I recommend this article.
At this point, if our TailwindCSS/Rails installation worked, we should see a nicely styled page. Let's test it out.
pages#index
view:Point the root route to the pages#index
view by putting root 'pages#index'
in the config/routes.rb
file.
The full file should look like this:
In new terminal tab, run bin/webpack-dev-server
$ bin/webpack-dev-server[output omitted]
This webpack server is what will compile TailwindCSS via PostCSS. It serves a minified version of the CSS we need and nothing else. This makes it fast for users and automatic for developers.
In another new terminal tab, run rails s
$ rails s[output omitted]
This will serve our Rails app.
Open a browser and go to localhost:3000
. You should see this:

Congrats, you just installed TailwindCSS on Rails 6. I'll leave it to you to decide whether the added technical lift is worth the effort.