Rails 8 introduces a significant change to the asset pipeline by making Propshaft the default asset pipeline, replacing Sprockets which has been the default since Rails 3.1.
This change reflects the modern approach to asset management in Rails applications.
What is Propshaft?
Propshaft is a lightweight asset pipeline that does one thing well: it fingerprints assets and serves them.
Unlike Sprockets which bundles, transpiles, and concatenates files, Propshaft delegates those responsibilities to specialized tools.
Here’s what each pipeline actually does:
| Feature | Sprockets | Propshaft |
|---|---|---|
| Asset fingerprinting | Yes | Yes |
Manifest directives (//= require) |
Yes | No |
| Sass/SCSS compilation | Yes | No |
| CoffeeScript compilation | Yes | No |
| JavaScript bundling | Yes | No |
| Source maps | Limited | No |
Propshaft expects us to use dedicated tools:
- JavaScript: Import Maps, esbuild, or Webpack via
jsbundling-rails - CSS: Tailwind, Sass via
cssbundling-rails, or plain CSS
Migrating a Rails 7.1 app to Propshaft
Let’s walk through migrating a typical Rails 7.1 application that uses Bootstrap, jQuery, Select2, and Font Awesome.
Step 1: Audit current asset setup
Before migrating, we need to understand what our app uses:
# Find Sprockets directives
grep -r "//= require" app/assets/javascripts/
grep -r "*= require" app/assets/stylesheets/
# Check for preprocessed files
find app/assets -name "*.scss" -o -name "*.sass" -o -name "*.coffee"Step 2: Update the Gemfile
# Remove Sprockets gems
gem "sprockets-rails" # DELETE
gem "sassc-rails" # DELETE if present
gem "jquery-rails" # DELETE - we'll use npm instead
gem "bootstrap" # DELETE - we'll use npm instead
# Add Propshaft
gem "propshaft"
# Add JavaScript bundling (required if using npm packages)
gem "jsbundling-rails"
# Add CSS bundling (ONLY if using Sass/SCSS files)
# Skip this if using plain CSS or Tailwind
gem "cssbundling-rails"Run the installation:
bundle install
rails javascript:install:esbuildIf using Sass/SCSS files, also run:
rails css:install:sassIf using plain CSS,
we can skip cssbundling-rails entirely.
Propshaft serves .css files directly without any build step.
Step 3: Install npm packages
Replace gem-based assets with npm packages:
# Core libraries
yarn add @hotwired/turbo-rails @hotwired/stimulus
# Bootstrap (previously bootstrap gem)
yarn add bootstrap @popperjs/core
# jQuery (previously jquery-rails gem)
yarn add jquery
# Select2 (previously select2-rails gem)
yarn add select2
# Font Awesome (previously font-awesome-rails gem)
yarn add @fortawesome/fontawesome-freeMigrating popular libraries
Bootstrap migration
Before (Sprockets with bootstrap gem):
// app/assets/javascripts/application.js
//= require jquery
//= require popper
//= require bootstrap// app/assets/stylesheets/application.scss
@import "bootstrap";After (Propshaft with npm):
// app/javascript/application.js
import * as bootstrap from "bootstrap"
// Initialize tooltips globally
document.addEventListener("turbo:load", () => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el))
})// app/assets/stylesheets/application.sass.scss
@import "bootstrap/scss/bootstrap";jQuery migration
Before (Sprockets with jquery-rails gem):
// app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
$(document).ready(function() {
$(".dropdown").dropdown();
});After (Propshaft with npm):
// app/javascript/application.js
import jquery from "jquery"
window.jQuery = jquery
window.$ = jquery
document.addEventListener("turbo:load", () => {
$(".dropdown").dropdown()
})Note: We expose jQuery globally because many plugins expect $ and jQuery on window.
Select2 migration
Before (Sprockets with select2-rails gem):
// app/assets/javascripts/application.js
//= require select2
$(document).ready(function() {
$(".select2").select2();
});// app/assets/stylesheets/application.scss
//= require select2After (Propshaft with npm):
// app/javascript/application.js
import jquery from "jquery"
window.jQuery = jquery
window.$ = jquery
import select2 from "select2"
select2($) // Initialize Select2 with jQuery
document.addEventListener("turbo:load", () => {
$(".select2").select2({
theme: "bootstrap-5"
})
})// app/assets/stylesheets/application.sass.scss
@import "select2/dist/css/select2.css";Font Awesome migration
Before (Sprockets with font-awesome-rails gem):
// app/assets/stylesheets/application.scss
@import "font-awesome";After (Propshaft with npm):
// app/assets/stylesheets/application.sass.scss
@import "@fortawesome/fontawesome-free/css/all.css";For the webfonts, copy them to the assets folder or configure the path:
# Copy font files to assets
cp -r node_modules/@fortawesome/fontawesome-free/webfonts app/assets/Or override the font path in Sass:
// app/assets/stylesheets/application.sass.scss
$fa-font-path: "../webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/brands";Stimulus controllers
Before (Sprockets):
// app/assets/javascripts/controllers/hello_controller.js
//= require stimulus
import { Controller } from "stimulus"
export default class extends Controller {
connect() {
this.element.textContent = "Hello World!"
}
}After (Propshaft):
// app/javascript/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.textContent = "Hello World!"
}
}// app/javascript/controllers/index.js
import { application } from "./application"
import HelloController from "./hello_controller"
application.register("hello", HelloController)Handling gems without npm equivalents
Some Rails gems bundle JavaScript assets that don’t have npm packages.
The cocoon gem for nested forms is a common example.
1. Vendor the JavaScript file
Copy the JavaScript from the gem into our app:
# Find and copy the gem's JavaScript
cp $(bundle show cocoon)/app/assets/javascripts/cocoon.js app/javascript/vendor/Then import it in app/javascript/application.js:
import "./vendor/cocoon"2. Replace with a Stimulus controller
Write a Stimulus controller that replicates the gem’s functionality.
For cocoon,
create a nested_form_controller.js that handles adding/removing nested fields.
This is more work upfront but gives us full control and removes the gem dependency.
3. Use Import Maps (if gem supports it)
# config/importmap.rb
pin "cocoon", to: "cocoon.js", preload: trueThis only works if the gem places JavaScript where Propshaft can find it.
Common gems and their alternatives:
| Gem | npm Package | Alternative |
|---|---|---|
| jquery-rails | jquery | Use npm |
| bootstrap | bootstrap | Use npm |
| select2-rails | select2 | Use npm |
| cocoon | None | Vendor file or Stimulus |
| best_in_place | None | Stimulus controller |
| chartkick | chartkick | Use npm |
| flatpickr | flatpickr | Use npm |
| toastr-rails | toastr | Use npm |
Handling CSS asset paths
Images in CSS
Before (Sprockets):
.hero {
background-image: image-url("hero-bg.jpg");
}
.logo {
background-image: asset-url("logo.png");
}After (Propshaft):
.hero {
background-image: url("hero-bg.jpg");
}
.logo {
background-image: url("logo.png");
}Propshaft resolves paths relative to the app/assets/images/ directory.
Custom fonts
Before (Sprockets):
@font-face {
font-family: "CustomFont";
src: font-url("custom-font.woff2") format("woff2");
}After (Propshaft):
Place fonts in app/assets/fonts/ and reference them:
@font-face {
font-family: "CustomFont";
src: url("custom-font.woff2") format("woff2");
}Update layout and configuration
Layout file
<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
<head>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
</head>
<body>
<%= yield %>
</body>
</html>Remove Sprockets configuration
Delete config/initializers/assets.rb and remove these from environment files:
# config/environments/production.rb
# DELETE these lines:
config.assets.compile = false
config.assets.js_compressor = :terser
config.assets.css_compressor = :sassBuild scripts
The installers create these automatically in package.json:
{
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets",
"build:css": "sass ./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
}
}Common errors and fixes
jQuery plugin not working
TypeError: $(...).select2 is not a functionCause: jQuery not exposed globally.
Fix: Add to application.js:
import jquery from "jquery"
window.jQuery = jquery
window.$ = jqueryBootstrap JavaScript not initializing
Uncaught TypeError: bootstrap.Tooltip is not a constructorCause: Bootstrap not imported correctly.
Fix: Import the full Bootstrap bundle:
import * as bootstrap from "bootstrap"
window.bootstrap = bootstrapFont Awesome icons not showing
Cause: Webfont files not found.
Fix: Either copy webfonts to assets or use the CSS version:
// Use pre-built CSS instead of Sass
@import "@fortawesome/fontawesome-free/css/all.css";Sass variables undefined
SassError: Undefined variable: "$primary"Cause: Bootstrap variables not loaded before custom styles.
Fix: Import Bootstrap functions and variables first:
// app/assets/stylesheets/application.sass.scss
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "variables"; // Our custom variables
@import "bootstrap/scss/bootstrap";When to stay with Sprockets
Consider keeping Sprockets if:
-
Using CoffeeScript extensively: Requires rewriting all files to JavaScript.
-
Relying on asset gems without npm alternatives: Some older gems only work with Sprockets.
-
Complex custom Sprockets processors: Need to reimplement in build tools.
To use Sprockets in Rails 8:
rails new myapp --asset-pipeline=sprocketsPros and cons
Pros:
- 3-5x faster asset compilation in development
- Access to full npm ecosystem
- Modern ES modules instead of global scripts
- Simpler debugging - source files served directly
- Better alignment with frontend tooling
Cons:
- Migration effort for existing apps
- Need to learn npm/yarn workflow
- Some gems don’t have npm equivalents
- Additional build step for CSS/JS
Conclusion
Propshaft works best when we embrace the npm ecosystem. Most popular libraries like Bootstrap, jQuery, and Select2 have npm packages that work better than their gem counterparts.
For new apps, Propshaft with esbuild provides a modern, fast setup.
For existing apps, the migration is straightforward if we’re already using common libraries. The main work is replacing gem-based assets with npm packages.
