Troubleshooting
Diagnose the symptoms that are easy to misread as "the gem is broken."
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.