Understanding importmap-rails
Source: Dev.to
Introduction
If you’ve worked with modern JavaScript, you’re familiar with ES modules and import statements. Rails apps can use esbuild (or vite or bun) for this, but the default (Rails‑way) is importmap‑rails. It lets you write
import { Controller } from "@hotwired/stimulus"
without any build step.
What are import maps?
Import maps are a web standard that tells browsers how to resolve bare module specifiers (e.g., import React from "react"). Browsers need an absolute path, a relative path, or an HTTP URL to load a module.
An import map provides the translation, for example:
{
"imports": {
"application": "/assets/application-abc123.js",
"@hotwired/stimulus": "/assets/stimulus.min-def456.js",
"controllers/application": "/assets/controllers/application-ghi789.js"
}
}
When your JavaScript says import { Controller } from "@hotwired/stimulus", the browser looks up "@hotwired/stimulus" in this map and loads /assets/stimulus.min-def456.js.
Using importmap‑rails
The importmap-rails gem generates the “ tag for you. Include it in your layout with:
This helper reads config/importmap.rb, outputs the import map, adds modulepreload links (so the browser starts downloading your JavaScript files immediately), and inserts your application entry point.
Configuring pins
In config/importmap.rb you define what goes in the map:
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@rails/request.js", to: "@rails--request.js.js"
Each pin creates a mapping (entry) in the import map:
- First argument – the bare module specifier you’ll use in
importstatements. to:attribute – the file that should be loaded from the asset pipeline (usually underapp/javascriptorvendor/javascript).
Pinning packages from npm
To add a package from npm, run:
./bin/importmap pin package-name
The command downloads the package file into vendor/javascript and adds the pin to config/importmap.rb. By default it uses JSPM.org as the CDN, but you can specify another source:
./bin/importmap pin react --from unpkg
./bin/importmap pin react --from jsdelivr
The downloaded files are checked into source control and served through your app’s asset pipeline.
Pinning entire directories
Instead of pinning files individually, you can map whole directories:
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/turbo_stream_actions", under: "turbo_stream_actions"
The under: attribute creates a namespace prefix. Every file in the directory becomes importable with that prefix.
-
app/javascript/controllers/reposition_controller.js→import RepositionController from "controllers/reposition_controller" -
app/javascript/turbo_stream_actions/set_data_attribute.js→import set_data_attribute from "turbo_stream_actions/set_data_attribute"
Custom Turbo Stream actions
-
Map the directory in
config/importmap.rb:pin_all_from "app/javascript/turbo_stream_actions", under: "turbo_stream_actions" -
When Rails generates the import map (“), it scans that directory and creates entries such as:
{ "imports": { "turbo_stream_actions": "/assets/turbo_stream_actions/index-abc.js", "turbo_stream_actions/set_data_attribute": "/assets/turbo_stream_actions/set_data_attribute-xyz.js" } } -
Create the custom action (
app/javascript/turbo_stream_actions/set_data_attribute.js):export default function() { // your custom logic } -
Register it in
app/javascript/turbo_stream_actions/index.js:import set_data_attribute from "turbo_stream_actions/set_data_attribute" Turbo.StreamActions.set_data_attribute = set_data_attribute -
Load the actions in your main entry point (
app/javascript/application.js):import "turbo_stream_actions"
The browser resolves "turbo_stream_actions/set_data_attribute" via the import map, fetches the compiled asset, and the action becomes available.
Stimulus integration
-
Pin the controllers directory:
pin_all_from "app/javascript/controllers", under: "controllers" -
Bootstrap Stimulus in
app/javascript/controllers/index.js:import { application } from "controllers/application" import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application) -
Define the application (
app/javascript/controllers/application.js):import { Application } from "@hotwired/stimulus" const application = Application.start() application.debug = false window.Stimulus = application export { application }
The eagerLoadControllersFrom function scans the import map for entries that start with "controllers/" and automatically imports and registers them. Adding a new controller file under app/javascript/controllers makes it instantly available—no extra configuration needed.
Summary
- Import maps let browsers resolve bare module specifiers without a bundler.
- importmap‑rails generates the import map and
modulepreloadlinks for Rails apps. - Use
pinfor individual files,pin_all_fromfor whole directories, and./bin/importmap pinto pull packages from npm. - The
under:namespace creates a clean import prefix that mirrors your directory structure, simplifying the integration of Turbo Stream actions, Stimulus controllers, and any other JavaScript modules.