Troubleshooting
Diagnose the symptoms that are easy to misread as "the gem is broken."
When a component's JavaScript fails
Since v0.73.0 a broken or missing controller or shared module reports itself, naming the component and the file, so you read the message instead of guessing. Before 0.73.0 these failures were silent: the component rendered and its behaviour was simply off.
Read the console first
Every message is tagged. The format is:
[RapidRailsUI] <component>#<element id>: <message> (<cause>)
[RapidRailsUI] <component> <tag.class.class>: <message> (<cause>) # element without an id
The messages you will see, and what each means:
-
stimulus_guard: could not load rapid_rails_ui/stimulus_guard; no RapidRailsUI controller will work. Run bin/rails rui:doctor (<browser error>)
The guard's import chain failed, so nothing registered. This line names the module the dynamic import asked for (rapid_rails_ui/stimulus_guard). The browser's own 404 line, directly above it, names the file that actually failed to load. Read both: the first tells you the gem's entry point did not come up, the second tells you which file in the chain is missing. -
stimulus_guard: could not register <id>; that component's behaviour is off on this page (<cause>)
One controller failed to import or register. Every other controller still registered. The cause is usually a stale copy or a module that controller imports. -
<id>#<element>: controller never connected; check app/javascript/controllers/rapid_rails_ui/<name>_controller.js exists and is registered
The page has markup withdata-controller="<id>"but Stimulus never connected a controller to it. Reported 1.5 seconds after boot and after eachturbo:load. This is the message you get when a controller file was never copied into the app. -
chart#<id>: missing <what>; the markup predates this controller. Run bin/rails rui:update
The chart controller checks its markup inconnect(): the categories attribute on the figure, the tooltip target, and the rest. A miss means the rendered HTML and the copied controller are from different gem versions. One exception: 0.73.0 itself loggedmissing data-chart-series on the figureon every chart because the check read the wrong attribute. That is the gem, not your app; upgrade to 0.73.1.
On the page
The affected element gets data-rui-error="<component>: <message>" in every environment, so you can find it with the inspector. In development the component also gets a visible notice prepended inside it: a div[data-rui-notice][role=alert] reading [RapidRailsUI] <message>. Run bin/rails rui:doctor. The notice needs the layout to say which environment it is in:
<%= rui_env_meta %>
The helper renders <meta name="rui-env" content="development"> in development and nothing anywhere else, so production pages carry no notice and no extra markup.
Run the doctor
bin/rails rui:doctor compares the app's copies of the gem's JavaScript with the installed gem and tells you what to fix. On a healthy app it prints its header and an ok: line, and exits 0:
RapidRailsUI doctor (bundler: importmap)
ok: every copied controller and shared module matches the gem; registration block present.
With findings it exits 1 and prints one line per finding, then the fix:
RapidRailsUI doctor (bundler: importmap)
stale: app/javascript/controllers/rapid_rails_ui/chart_controller.js — differs from the installed gem
missing: app/javascript/controllers/rapid_rails_ui/shared/diagnostics.js — not copied
old_block: app/javascript/controllers/index.js — registration block predates 0.73.0
fix: bin/rails rui:update (or rails g rapid_rails_ui:upgrade)
hint: add <%= rui_env_meta %> to the <head> of app/views/layouts/admin.html.erb, app/views/layouts/blog.html.erb so broken components show a notice on the page in development
The hint (0.73.1+) checks every layout under app/views/layouts that loads JavaScript, follows a rendered head partial, and names the layouts without the tag. It is advice, not a finding: a layout that renders no components can go without, and the exit code stays 0.
Finding kinds are missing, stale, unrewritten (a relative import left in a copy on an importmap host), unregistered (the guard registry lacks a controller), no_block and old_block (the managed index.js block), and no_pin. In every case the fix line is the same command. Run it, then run the doctor again until it says ok.
Order of operations after any gem upgrade: bundle update rapid_rails_ui, then bin/rails rui:update, then bin/rails rui:doctor. The doctor is the check that the other two did their job.
Component renders but interactions do nothing
Symptom. A dialog/menu/accordion/etc. renders fine, looks fine, but clicking the trigger does nothing. No errors in the browser console.
Root cause (99% of the time). The Stimulus identifier on the rendered HTML does not match
the identifier your controllers/index.js registered. Stimulus is silent about this: no controller
matches the element, so nothing fires.
Diagnosis ladder
-
Inspect the rendered DOM. Find the component's wrapper element and read the
data-controllerattribute. That string is the source of truth. -
Compare it to
app/javascript/controllers/index.js. Find theapplication.register("...", XController)line for that component. The first argument must match exactly.- If the DOM says
data-controller="dialog", you needapplication.register("dialog", ...). - If the DOM says
data-controller="rui--dialog", you needapplication.register("rui--dialog", ...).
- If the DOM says
-
Check
config.stimulus_namespaceinconfig/initializers/rapid_rails_ui.rb. If it's set to"rui", every gem-emitted identifier is prefixed withrui--. The cleanest fix is to re-run the installer with--stimulus-namespace=rui, which rewritesindex.jsto match. See Stimulus Namespace . -
Custom triggers in your own templates. If you wrote
<button data-action="dialog#open">outside the component slot, that action string must use the same identifier the gem emits. Withstimulus_namespace = "rui", writedata-action="rui--dialog#open"— or use thestimulus_action(:dialog, "click->#open")helper, which adapts automatically. - Every controller on every page is dead, not just one. That's a different bug class — see "One 404 kills ALL Stimulus" below before assuming it's a namespace mismatch.
Shipped since v0.49.0: guardCollisions(application) wraps
application.register so a duplicate Stimulus identifier throws at boot — naming both the
controller that won the registration and the one that lost — instead of one of them silently going dead.
It's part of the managed block in controllers/index.js; see
Stimulus Namespace
for the
exact block shape.
Controller copies drift after bundle update
Symptom. You bundle update rapid_rails_ui, the Ruby side clearly changed
(new component, changelog entry, gemspec version bump), but the browser behaves like nothing happened — a
documented fix doesn't show up, or a brand-new component (e.g. rui_tabs in v0.52.0) 404s on its
Stimulus controller.
Root cause. On jsbundling-rails hosts, esbuild can't resolve JS straight
from the gem — the install generator copies controller files into
app/javascript/controllers/rapid_rails_ui/ at install time. A plain bundle update
only touches the Ruby gem in vendor/.bundle; it never re-copies those JS files. The
gem's own post_install_message says as much, but it's easy to miss in a long bundler output.
Fix. Run the sync step after every bundle update rapid_rails_ui:
bin/rails rui:update
rui:update (v0.52.0+) touches only the gem-owned controller files and the managed
index.js block, then runs yarn build if one is configured, and prints exactly which
files changed. rails g rapid_rails_ui:install does the same sync plus the full install flow if
you'd rather re-run the generator. Importmap-rails hosts don't need either — controllers
resolve straight from the gem via pin_all_from, so bundle update alone is enough.
One 404 kills ALL Stimulus (importmap hosts)
Symptom. Every RapidRailsUI component on every page is simultaneously non-interactive —
not one broken component, all of them — usually right after a controller resync
(rui:update/install generator re-run, or an importmap pin refresh). No visible error unless you
check the browser console or Network tab.
Root cause. A handful of gem controllers import a shared helper via a relative,
extensionless specifier (e.g. import Foo from "./shared/turbo_fetch"). That resolves fine under
esbuild (jsbundling), but a browser's native ES module loader on an importmap host requests the
literal relative URL — unresolved by the import map, no digest — and gets a 404. Native ESM's import
graph is static and all-or-nothing per entry point: one failed transitive import aborts loading the whole
module, so controllers/index.js never finishes executing and not a single controller
registers. This exact failure hit this docs site's own importmap host after the v0.53.0 controller
resync (alert/kanban/editable's shared turbo_fetch import) — every docs page was dead until
fixed.
Diagnosis. Open DevTools → Network, filter to JS, reload. Every RRUI-pinned asset should
have a digest in its URL (e.g. /assets/controllers/rapid_rails_ui/dialog_controller-a1b2c3….js).
A 404 on a request without a digest — often ending in a bare path like
.../shared/turbo_fetch — is the tell: that import was never resolved through the import map at
all.
Fix. Pin the offending specifier the same way stimulus_guard is already pinned
— as a full controllers/rapid_rails_ui/... path, not a relative one — in your host's local
controller copy. If you hit this on a current gem version, it's worth filing upstream: the durable fix is
the install generator transforming relative imports for importmap hosts automatically, with a regression
test, so no host has to patch it by hand.
esbuild fails: "The symbol X has already been declared"
Your app already declares a JS class named AccordionController, DialogController, etc.,
and esbuild trips when the gem imports its same-named class into the same module.
Re-run the installer with the namespace-imports flag (v0.46.0+):
rails g rapid_rails_ui:install --namespace-imports=Rui
Full context on the Installation page (advanced section).
Native inputs render alongside the gem's styled inputs
A bare <input type="checkbox"> sits next to (or on top of) the gem's styled checkbox. Same
pattern can appear with radio, text, or search inputs.
Root cause: your host app has an unscoped global rule like input[type="checkbox"] { ... } that wins
on specificity against the gem's .sr-only. Surgical fix: exclude the gem's hidden inputs by adding
:not(.sr-only):not(.peer) to each selector. Full walkthrough on the
Installation page
.
Legacy select/form enhancer double-wraps RRUI's own controls
Symptom. A jQuery-era form enhancer — Materialize's FormSelect, Select2,
Chosen, and similar libraries that re-scan the whole document for select/input
elements on every Turbo navigation — wraps one of RapidRailsUI's own inputs or selects, breaking its styling
and Stimulus wiring.
Root cause. Those libraries have no way to distinguish RRUI's markup from your app's own unstyled native controls, so they enhance everything they find.
Fix (v0.51.2+). Select, Input, Textarea,
Checkbox, RadioButton, Upload, and Date all render a
data-rui="<component>" marker on their control element(s). Exclude RRUI's elements from
the enhancer's scan instead of hand-configuring per-field opt-outs:
// Vanilla / most libraries:
const elements = document.querySelectorAll("select:not([data-rui])")
// Select2:
$("select:not([data-rui])").select2()
// Chosen:
$("select:not([data-rui])").chosen()
Same pattern for the input/textarea equivalents. Full recipe (including Date's
multiple marked elements) on the
Select docs
' Legacy
Select Enhancers section.
Validation message causes a layout shift (or an unexplained gap)
Symptom. Either an unexplained empty strip under every rui_input/
rui_textarea, or the opposite — the field visibly jumps down when a validation message appears.
Root cause. Every input/textarea renders a validation-message <p> by
default — it's the target the real-time validation Stimulus controller writes into, and it's also where a
server-side model error renders on a 422 re-render. Since v0.50.7 it's styled text-sm empty:hidden:
zero height while empty, and it only takes up space once populated with a message. That's a deliberate
trade-off — no permanent gap under valid fields, but the field height does still change by one line the
moment a message appears.
Fix. If the tiny reflow when a message appears is undesirable in your layout (dense filter bars, inline search inputs), remove the element entirely rather than fighting the reflow:
<%= rui_input(name: :search, type: :search, placeholder: "Search...", validation: false) %>
validation: false removes the message element from the DOM outright — use it where you never
want inline validation UI at all, not as a fix for the one-line reflow on fields that do need it.
CSS looks wrong after upgrading the gem
Symptom. A component renders with missing colors, unstyled borders, or otherwise looks
broken right after a bundle update rapid_rails_ui or after adopting a newly-released component.
Root cause. Almost always a stale compiled CSS artifact, not a real regression — your build
output was compiled against the previous gem version's class list and Tailwind's JIT scan hasn't re-run
against the new one yet. A distant second cause: the new classes genuinely aren't in your safelist/manifest
(rare — the gem ships tailwind_manifest.rb/safelist.css precisely so hosts don't
need a hand-maintained safelist, but a version skip can occasionally outrun it).
Fix. Force a clean rebuild before assuming it's a real bug:
bin/rails tailwindcss:clobber # removes compiled CSS builds
bin/rails tailwindcss:build
rm -f app/assets/builds/*.css app/assets/builds/*.js app/assets/builds/*.map
yarn build:css && yarn build
Hard-refresh the browser afterward (stale cached CSS is indistinguishable from stale compiled CSS at a
glance). If specific classes are still visibly missing after a genuinely clean rebuild, check that your
Tailwind content glob includes the gem's tailwind_manifest.rb — see the
Installation page
.
Foreman: duplicate process names silently shadow a watcher
Symptom. bin/dev starts cleanly, no errors — but one watcher's output never
appears in the interleaved log, and edits in its domain (CSS, or a jsbundling JS watch) never trigger a
rebuild. Everything else works, which makes it easy to blame the file you were just editing instead of your
process runner.
Root cause. Foreman parses Procfile.dev into a name-keyed process list. Two
lines sharing the same process name — easy to introduce when merging setup steps from two guides, or adding
a second watcher and reusing a name out of habit (css: appearing twice, for instance) — silently
overwrite each other during parsing. Foreman never warns about the collision; it just runs the survivor and
the earlier line never starts.
Fix. Give every line in Procfile.dev a distinct name and confirm the process
list Foreman actually launched matches what you expect:
web: bin/rails server
css: bin/rails tailwindcss:watch
js: yarn build:watch
If you're not sure which processes actually started, the interleaved bin/dev log prefixes every
line with its process name and a color — count the distinct prefixes against your Procfile's line count.