Installation
Install Monsoon in Rails ERB
Monsoon needs no gem and no JavaScript bundler in Rails. Link it from the application layout with stylesheet_link_tag, either straight from the CDN or as a vendored file that Propshaft fingerprints.
Install
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "My app" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= javascript_tag nonce: true do %>(function(){var t;try{t=localStorage.getItem('monsoon-theme')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.add(d?'dark':'light')})()<% end %>
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body class="bg-background text-foreground antialiased">
<%= yield %>
</body>
</html>Vendor the file so Propshaft serves it with a digest:
npm install monsooncss
cp node_modules/monsooncss/dist/monsoon.min.css vendor/assets/stylesheets/monsoon.cssWithout Node.js, download it instead: curl -o vendor/assets/stylesheets/monsoon.css https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css.
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "My app" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= javascript_tag nonce: true do %>(function(){var t;try{t=localStorage.getItem('monsoon-theme')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.add(d?'dark':'light')})()<% end %>
<%= stylesheet_link_tag "monsoon", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body class="bg-background text-foreground antialiased">
<%= yield %>
</body>
</html>Using cssbundling-rails? Then @import "monsooncss"; at the top of the PostCSS entry resolves the package directly. Importmaps only handle JavaScript, so with importmap-rails always link the CSS with stylesheet_link_tag.
The vendored file goes in vendor/assets/stylesheets rather than app/assets/stylesheets, because stylesheet_link_tag :app links every file in the latter in alphabetical order, which could load Monsoon after your own styles.
If you enable the content security policy initializer, allow the CDN with policy.style_src :self, "https://cdn.jsdelivr.net", and add :data to img_src for the inline SVG images some styles use. nonce: true already covers the head script.
Use the classes
The class_names helper (also available as token_list) builds a class string from conditions, with each key a full class list:
<article class="<%= class_names("rounded-2xl border bg-surface p-6 space-y-4", "ring-2 ring-ring": plan.featured?, "shadow-sm": !plan.featured?) %>">
<h2 class="text-lg font-semibold"><%= plan.name %></h2>
<p class="text-3xl font-bold"><%= number_to_currency(plan.price) %></p>
<%= link_to "Choose #{plan.name}", new_subscription_path(plan: plan),
class: plan.featured? ? "block rounded-lg bg-primary text-primary-foreground px-4 py-2 text-center font-medium hover:bg-primary/90" : "block rounded-lg bg-secondary text-secondary-foreground px-4 py-2 text-center font-medium hover:bg-secondary/80" %>
</article><main class="mx-auto max-w-5xl px-6 py-12">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<%= render @plans %>
</div>
</main>Form builders take the same strings: form.text_field :email, class: "w-full rounded-lg border bg-background px-3 py-2 text-sm".
Dark mode
A Stimulus controller is the idiomatic home for the toggle. With importmap-rails and the default controllers/index.js, it registers itself from its file name:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
toggle() {
const root = document.documentElement
const next = root.classList.contains("dark") ? "light" : "dark"
root.classList.remove("dark", "light")
root.classList.add(next)
this.element.setAttribute("aria-pressed", String(next === "dark"))
try { localStorage.setItem("monsoon-theme", next) } catch (e) {}
}
}<button type="button" data-controller="theme" data-action="theme#toggle" class="rounded-lg border px-3 py-2 text-sm hover:bg-muted">
Toggle theme
</button>Turbo Drive swaps <body> and merges <head> on navigation but leaves the <html> element alone, so the class survives page visits.
Theme with --seed
Validate the stored color on the model, then write it inline in the view:
class Account < ApplicationRecord
validates :brand_color, format: { with: /\A#\h{6}\z/ }, allow_blank: true
end<%= tag.section style: "--seed: #{current_account.brand_color.presence || '#0d9488'}", class: "rounded-2xl bg-surface p-6 space-y-4" do %>
<h2 class="text-xl font-bold text-primary"><%= current_account.name %></h2>
<%= link_to "Settings", edit_account_path, class: "inline-block rounded-lg bg-primary text-primary-foreground px-4 py-2" %>
<% end %>Every semantic token (primary, secondary, accent, surfaces, muted text, borders and the focus ring) is derived from --seed in the browser. Theming lists the tokens.
Using the purge CLI
Scan views, helpers, components and Stimulus controllers:
{
"private": true,
"scripts": {
"css:purge": "monsooncss purge \"app/views/**/*.erb\" \"app/helpers/**/*.rb\" \"app/components/**/*.{rb,erb}\" \"app/javascript/**/*.js\" -o vendor/assets/stylesheets/monsoon.purged.css"
},
"devDependencies": {
"monsooncss": "^0.2.0"
}
}Link the purged file in production and keep the full one in development:
<%= stylesheet_link_tag(Rails.env.production? ? "monsoon.purged" : "monsoon", "data-turbo-track": "reload") %>Run npm run css:purge before bin/rails assets:precompile in your deploy (for example in the Dockerfile, right before the precompile step).
The purge CLI reads your files as plain text and keeps every class name it can see. It cannot see a name that is assembled at runtime, so write each class in full: isActive ? 'bg-primary' : 'bg-muted' survives a purge, 'bg-' + color does not. Names that only exist at runtime (from a database, a CMS or an API) go in --safelist bg-success,bg-danger. Class strings built in Ruby, like "text-#{tone}", have the same problem.
The purged file always keeps the reset, the theme tokens, the .dark and .light scopes and the keyframes, so dark mode and --seed keep working. See the purge CLI reference for every option.
Next steps
Frequently asked questions
Do I need a gem for Monsoon in Rails?
No. It is one stylesheet. Link it from the CDN or vendor the file and Propshaft serves it.
Does Monsoon work with ViewComponent?
Yes. Component templates are ERB like any other view; add app/components/** to the purge glob, as above.