How to Install Shoelace with Rails 7, esbuild, and PostCSS

12/17 Update: I had incorrectly used an import path for setBasePath below which inadvertently included the entire Shoelace library. Please review the updated import path for an optimized bundle size!

Rails 7 ships by default with a frontend pipeline based on import maps and Sprockets. I'm going on public record here that I don't like it, not at all. I ran into several insurmountable problems attempting to "pin" Shoelace and install my pick of components as well as load Shoelace's global set of CSS variables. Boo.

Thankfully, what is far more appealing regarding Rails 7 is its additional support for esbuild and PostCSS, two very fast, very capable, and extremely customizable frontend build tools. While I'm bummed that there's no real config file shipping out of the box for esbuild, such things can be put together with the right resources. Maybe the community can step up.

In the meantime, you can definitely use this setup as-is for common use cases such as installing Shoelace. There are however a couple of gotchas which I'll help you resolve herein.

Setting Up Rails

First, create a new Rails 7 app using this command:

rails new rails7demo -j esbuild -c postcss

The -j esbuild argument tells Rails you'd like to use esbuild for JavaScript bundling, and -c postcss for CSS bundling.

From here on, you can simply run bin/dev to boot up Rails along with both esbuild and PostCSS watch processes.

Installing Shoelace

This part is very easy! Simply run this command:

yarn add @shoelace-style/shoelace

Next, you can add a few components to your app/javascript/application.js file:

import "@shoelace-style/shoelace/dist/components/button/button.js"
import "@shoelace-style/shoelace/dist/components/icon/icon.js"
import "@shoelace-style/shoelace/dist/components/spinner/spinner.js"

and make the site styles look a bit better overall via app/assets/stylesheets/application.postcss.css:

body {
  font-family: -apple-system, sans-serif;
  background: #eee;
}

Now, let's test Shoelace out in an HTML view. First, run:

bin/rails generate controller Articles index --skip-routes

and then we'll update the config/routes.rb file:

Rails.application.routes.draw do
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  root "articles#index"
end

We can add the Shoelace components to app/views/articles/index.html.erb:

<p><sl-button type="primary">
  <sl-icon slot="prefix" name="twitter"></sl-icon>
  Follow on Twitter
</sl-button></p>

<p><sl-spinner style="font-size: 3rem; --track-width: 6px;"></sl-spinner></p>

Run bin/dev and go to http://localhost:3000 and…oh no, that doesn't look right at all! We forgot to add Shoelace's global stylesheet to include its CSS variables on the site!

Er, how do we do that? Hmm. If you try to import the stylesheet in the application.js file, esbuild and PostCSS will take turns clobbering the output application.css file in app/assets/builds. 🙁 And if you try to import the stylesheet directly inside of application.postcss.css, it doesn't work at all, because the PostCSS config doesn't do anything special to @import statements so you can't actually import anything from the node_modules folder. ☹️

Fixing the Import Problem 😃👍

There are two possible solutions to this:

One is to fix the esbuild/PostCSS clobbering issue by changing the output file in the build:css script inside package.json to something other than application.css, then adding a second stylesheet_link_tag to your application layout. This is probably the best solution overall. But I thought it would be worthwhile to see if we could keep the existing build configuration as-is, and simply fix the PostCSS import issue instead. So let's try that.

First, run this command:

yarn add postcss-import

Next, update your postcss.config.js file so it looks like this:

const atImport = require("postcss-import")

module.exports = {
  plugins: [
    atImport,
    require('postcss-nesting'),
    require('autoprefixer'),
  ],
}

Then add this to the top of your application.postcss.css file:

@import "@shoelace-style/shoelace/dist/themes/light.css";

Now when you boot up your server and try the site again, it should actually work this time! Except…the button is missing its icon. Where's the icon?!

We need to set up a process whereby we copy the icons out of Shoelace's assets folder, and then we tell Shoelace how to find them.

Copying Icon Assets

We'll do this the easy way. Since it's unlikely for Shoelace icons to change with any frequency, we don't need to worry about fingerprinting them for cache busting purposes. We can just dump them in public and call it a day.

First, update your scripts in package.json so they look like this:

"scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "yarn shoelace:copy-assets && postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css",
    "shoelace:copy-assets": "mkdir -p public/shoelace-assets && cp -r node_modules/@shoelace-style/shoelace/dist/assets public/shoelace-assets"
  }

All we did here is add yarn shoelace:copy-assets in front of the PostCSS command, and then in the copy-assets script we create a new folder and copy the files out of node_modules. We do this every time we boot up the site, so in future if you upgrade Shoelace, you'll always have the most up-to-date icon set.

Next, we'll add the following to application.js so Shoelace knows where to find the icon assets:

import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js"
setBasePath("/shoelace-assets")

Run bin/dev and presto! Your Shoelace button now has a Twitter icon to go with it.

And that's how you can use Shoelace components with your shiny new Rails 7 + esbuild + PostCSS app. Enjoy! 🥳

34