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

  1. Inspect the rendered DOM. Find the component's wrapper element and read the data-controller attribute. That string is the source of truth.
  2. Compare it to app/javascript/controllers/index.js. Find the application.register("...", XController) line for that component. The first argument must match exactly.
    • If the DOM says data-controller="dialog", you need application.register("dialog", ...).
    • If the DOM says data-controller="rui--dialog", you need application.register("rui--dialog", ...).
  3. Check config.stimulus_namespace in config/initializers/rapid_rails_ui.rb. If it's set to "rui", every gem-emitted identifier is prefixed with rui--. The cleanest fix is to re-run the installer with --stimulus-namespace=rui, which rewrites index.js to match. See Stimulus Namespace .
  4. 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. With stimulus_namespace = "rui", write data-action="rui--dialog#open" — or use the stimulus_action(:dialog, "click->#open") helper, which adapts automatically.
  5. 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:

Sync JS controller copies (jsbundling only)
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+):

Prefix the gem's JS local names
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:

Exclude RRUI selects from a legacy enhancer
// 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:

Opt out entirely — no placeholder, no 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:

tailwindcss-rails hosts
bin/rails tailwindcss:clobber   # removes compiled CSS builds
bin/rails tailwindcss:build
jsbundling hosts (esbuild + a separate CSS build script)
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:

Procfile.dev — distinct names
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.