Understanding importmap-rails

Published: (February 26, 2026 at 12:15 PM EST)
4 min read
Source: Dev.to

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 import statements.
  • to: attribute – the file that should be loaded from the asset pipeline (usually under app/javascript or vendor/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

  1. Map the directory in config/importmap.rb:

    pin_all_from "app/javascript/turbo_stream_actions", under: "turbo_stream_actions"
  2. 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"
      }
    }
  3. Create the custom action (app/javascript/turbo_stream_actions/set_data_attribute.js):

    export default function() {
      // your custom logic
    }
  4. 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
  5. 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

  1. Pin the controllers directory:

    pin_all_from "app/javascript/controllers", under: "controllers"
  2. Bootstrap Stimulus in app/javascript/controllers/index.js:

    import { application } from "controllers/application"
    import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
    
    eagerLoadControllersFrom("controllers", application)
  3. 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 modulepreload links for Rails apps.
  • Use pin for individual files, pin_all_from for whole directories, and ./bin/importmap pin to 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.
0 views
Back to Blog

Related posts

Read more »

A Horror Story About JavaScript Promise

Before the Midnight Adventure – What’s a Promise? > “Don’t worry – it’s easier than facing a monster under the bed!” A Promise is an object that represents the...