Webpack isn't just for JavaScript. You can bundle images with it too. Webpacker makes it relatively easy to work with images, but it is admittedly confusing at first: Images in JavaScript?

In this post, we'll demonstrate how to reference Webpacker images from your JavaScript, CSS, and Rails views. The following examples were created using Rails 6 and Webpacker 4, but may work with other versions as well. Pre-requisites for working with Webpacker in a Rails project also include yarn.

Subscribe to my newsletter, Joy of Rails, to get notified about new content.

Folder structure

First, where should you put your images? It doesn't matter. The easiest place to start is under your app/javascript folder, the default source path for Webpacker, such as app/javascript/images.

For the rest of this guide, we'll assume the following directory structure and files:

app/javascript
├── components
│   └── Taco.js
├── css
│   ├── main.css
├── images
│   ├── burritos.jpg
│   ├── guacamole.jpg
│   └── tacos.jpg
└── packs
    └── application.js

Isn't weird to put images and css in a folder called "javascript"? Depends. If you consider, from webpack's perspective, everything is a JavaScript module, it may not be so strange. Otherwise, it's possible to rename app/javascript or place your images elsewhere. More on that below.

Images in JS

To reference an image from JavaScript in your Webpacker build, simply import it like any other module. React is not required for this to work ;)

// app/javascripts/components/Taco.js
import TacoImage from '../images/tacos.jpg'

export default function ({ title }) {
  return `
  <div>
    <h1>${title}</h1>
    <p><img src=${TacoImage} alt="Tacos, yum" /></p>
  </div>
  `
}

In the example above, webpack will import TacoImage as a url to the file. In other words, an "image module" in webpack exports a single default value, a string, representing the location of the file. Based on the default Webpacker configuration, the filename will look something like "/packs/media/images/tacos-abcd1234.jpg".

Importing a image also works if you're using "CSS in JS" to style a React component.

import React from 'react'

import TacoImage from '../images/tacos.jpg'

const styles = {
  backgroundImage: `url(${TacoImage})`,
}

export default function ({ title }) {
  return <div style={styles}>{title}!</div>
}

Images in CSS

In Sprockets, when referencing images in CSS, you would use a special image-url() helper. In webpack, simply use the standard url() expression in CSS with a relative path.

/* app/javascript/css/main.css */
.burritos {
  background-image: url('../images/burritos.jpg');
}

The output for the style rule will, again, look something like background-image: url(/packs/media/images/burritos-efgh5678.jpg);. This technique will also work for image paths in CSS Modules.

Images in CSS within NPM modules

One tricky bit worth mentioning is bundling images referenced in SCSS within an imported NPM module. For example, many jQuery plugins bundle their own SCSS and image assets. When webpack processes this vendored CSS, you may see an error like the following, like in this question on StackOverflow:

Module not found: Error: Can't resolve '../img/controls.png'

The problem is the path does not resolve properly relative to the output for this vendored SCSS. From the Webpacker docs:

Since Sass/libsass does not provide url rewriting, all linked assets must be relative to the output. Add the missing url rewriting using the resolve-url-loader. Place it directly after the sass-loader in the loader chain.

To fix this, you may need to get your hands dirty with some Webpacker configuration. Add the resolve-url-loader and configure in config/webpack/environment.js:

yarn add resolve-url-loader
// config/webpack/environment.js
const { environment } = require('@rails/webpacker')

// resolve-url-loader must be used before sass-loader
environment.loaders.get('sass').use.splice(-1, 0, {
  loader: 'resolve-url-loader',
})

This loader rule, inserted in the loader pipeline for SASS/SCSS files, will ensure the proper url is written to the CSS output by webpack.

Images in Rails views

You may be accustomed to <%= lazy_image_tag 'tacos.jpg' %> to reference a image bundled in the Rails asset pipeline. webpack has a similar tag:

<!-- app/views/lunches/index.html.erb -->

<%= image_pack_tag 'media/images/guacamole.jpg' %>

Note, since Webpacker 4, the prefix media/ is necessary and the remaining path represents the location from your webpack source path.

There's a catch. This change may result in the following error:

Webpacker::Manifest::MissingEntryError in Lunches#index
Showing /path/to/project/app/views/lunches/index.html.erb where line #4 raised:

Webpacker can't find media/images/guacamole.jpg in /path/to/project/public/packs/manifest.json.

However, if you use <%= image_pack_tag 'media/images/tacos.jpg %>, the taco image will happily renders. What gives?

Your Rails app is not being selective about cuisine. The difference is, we earlier imported the tacos.jpg image in webpack, but not guacamole.jpg.

One way to fix this issue is to import the guacamole.jpg image somewhere in your webpack dependency graph. It's not necessary to grab a reference to the imported variable because we only care about the side effect of emitting the file for Rails to reference in the view.

import '../images/guacamole.jpg'

Another way to fix this issue is to import all images in the app/javascript/images directory. webpack provides a special function to import many files in a directory in one expression: require.context. You might add this to your application.js pack:

// app/javascript/packs/application.js

require.context('../images', true)

This expression will recursively require all the files in the images directory. As a result, we can now render guacamole.jpg in a Rails view.

Note: I only recommend using require.context for your images if you need to render them in your Rails views; require.context is NOT necessary to import images into JS files like your React components, as illustrated earlier.

Reconfiguring

If you don't feel comfortable with app/javascript as the source directory for images, you can either rename the source path or add to the set of resolved paths.

To rename app/javascript, rename the directory and tell Rails about it in config/webpacker.yml

default: &default
  source_path: app/frontend

To add to the set of resolved paths where webpack should look for assets besides in app/javascript:

default: &default
  additional_paths:
    - app/assets

Diving Deeper

I have to admit, a few years ago, when I first heard about webpack, I was super-confused. I understood it to be a JavaScript module bundler. How on Earth does it handle images?

The short answer, of course, is it depends. Generally, webpack will treat everything it can understand as a JavaScript module. To help webpack understand images, projects would add a "loader" (or loaders) to the webpack configuration. A suitable loader would know how to handle an image file and output a representation of something, like an inlined base64 string, that can be manipulated in JavaScript.

To help webpack understand images, svg files, and fonts in your Rails project, Webpacker adds the file-loader package. This package will emit the imported file as a side effect of the build and return a path to the file as the module contents.

For more on how webpack works with images, check out the asset management docs.

I also put together a sample Rails 6 Webpacker demo project on GitHub for more context:

Discuss it on Twitter · Published on Dec 18, 2019

More posts

A guide to NPM version constraints for Rubyists

A reference guide to NPM version constraints for dependencies declared in the package.json file of a Rails project from the perspective of a Ruby developer familiar with similar conventions used to specify Ruby dependencies in a Gemfile.

3 ways webpack surprises web developers

When I first started working with webpack, I was in for a few surprises. I assumed how things should behave, based on my previous experience with the Rails asset pipeline, only to learn through experience how I was wrong.

Overpacking: A common Webpacker mistake

A big issue that comes up with Webpacker is "where to put the JavaScript files". This post demonstrates proper use of entry point files, called packs, with Webpacker and Rails and will help developers avoid a common gotcha.

Photo by JR Korpa on Unsplash