# RapidRailsUI > Production-ready ViewComponent UI library for Ruby on Rails with Tailwind CSS, Hotwire, and Stimulus JS. All helpers start with `rui_`. Designed for Rails 7+ without React or external JavaScript frameworks. ## About RapidRailsUI RapidRailsUI is a comprehensive component library that bridges the gap between Rails conventions and modern UI design. Built on ViewComponents, it provides 37 production-ready UI components that work seamlessly with Rails views, form builders, and Turbo/Hotwire. **Key Architecture**: - **ViewComponent-based**: Each component is an encapsulated, testable ViewComponent - **Tailwind CSS styling**: All styles defined via Tailwind utility classes in `styles.rb` - **Stimulus JS interactivity**: JavaScript behavior via Stimulus controllers (no plain JS) - **Form builder integration**: Use `f.rui_component` in form_with blocks - **Dark mode ready**: All components include dark mode variants automatically **Convention**: All helpers follow the `rui_` pattern for consistency. ## For AI Agents This file (`llms.txt`) is a concise index. Two other machine-readable surfaces go deeper — use whichever fits the context budget: - **llms-full.txt** — this file plus every component's full `USAGE.md`, verbatim, one `## ComponentName` section per component. Load this when `llms.txt`'s notes aren't enough detail to generate correct code. - **llms-mini.txt** — an ultra-compact table (helper, complexity, param/slot counts, AI-friendliness score) for tight context budgets. Good for a first pass over "which component do I need" before loading detail. - **The installed gem is the version-true fallback.** If your Rails app already has `rapid_rails_ui` in its Gemfile, every component ships a `USAGE.md` right next to its `component.rb` (e.g. `app/components/rapid_rails_ui/card/USAGE.md` inside the gem). Since that file ships with the exact gem version installed, it can be more current than a hosted copy of this file for an app pinned to an older release — prefer it when the installed version and this file's version might disagree. - **Every `/docs/` URL content-negotiates.** Request with `Accept: */*` (curl's default) to get compact JSON component metadata (params, slots, gotchas); request with `Accept: text/markdown` to get the component's docs as Markdown; browsers get the HTML page. Example: `curl -H "Accept: text/markdown" https://rapidrails.cc/docs/tabs`. A machine-readable index of all components lives at `https://rapidrails.cc/api/schema.json`. ## Key Conventions for AI Assistants When generating RapidRailsUI code, follow these critical patterns: ### Helper Method Naming All component helpers follow the pattern `rui_`: `rui_button`, `rui_link`, `rui_checkbox`, etc. ### Argument Order Follows Rails Conventions ```erb <%= rui_link("View Posts", posts_path) %> <%= rui_button("Click Me", color: :primary) %> <%= rui_button_to("Delete", post_path, method: :delete, color: :danger) %> <%= rui_link(posts_path, "View Posts") %> ``` ### Colors: Semantic + Tailwind Palette ```erb <%= rui_button("Submit", color: :primary) %> <%# semantic, recommended %> <%= rui_button("Action", color: :indigo) %> <%# Tailwind palette, also supported %> ``` Semantic: `:primary` `:secondary` `:tertiary` `:accent` `:success` `:danger` `:warning` `:info` `:muted` `:default`. Tailwind: `:zinc` `:gray` `:red` `:orange` `:amber` `:blue` `:green` `:indigo` `:violet` `:pink`, and more. ### Form Builder Integration ```erb <%= form_with(model: @post) do |f| %> <%= f.rui_input(:title, label: "Title") %> <%= f.rui_textarea(:body, label: "Body", rows: 8) %> <%= f.rui_checkbox(:published, label: "Publish this post") %> <%= f.rui_button %> <%# auto-generates "Create Post" / "Update Post" %> <% end %> ``` ### Dark Mode All components automatically support dark mode via Tailwind's `dark:` variants. No manual dark mode styling needed. ## Component Reference All components are documented at https://rapidrails.cc/docs ### Form Controls - [Button](https://rapidrails.cc/docs/button) - `rui_button` - Form submissions and primary actions - [ButtonTo](https://rapidrails.cc/docs/button_to) - `rui_button_to` - RESTful action buttons (POST/PATCH/DELETE) - [Checkbox](https://rapidrails.cc/docs/checkbox) - `rui_checkbox` - Single or collection checkboxes with form builder support - [RadioButton](https://rapidrails.cc/docs/radio_button) - `rui_radio_button` - Radio button groups with multiple variants - [Input](https://rapidrails.cc/docs/input) - `rui_input` - Text input field with multiple types (text, email, password, number, tel, url, search) - [Textarea](https://rapidrails.cc/docs/textarea) - `rui_textarea` - Multi-line text input with auto-resize and character counter - [Select](https://rapidrails.cc/docs/select) - `rui_select` - Native HTML select. `f.rui_select(:status, ["Draft", "Published"])` or `f.rui_select(:author_id, Author.all, :id, :name)` (Rails collection_select style) - [Combobox](https://rapidrails.cc/docs/combobox) - `rui_combobox` - Searchable dropdown with single/multi-select, collection config, and keyboard navigation **Pro tier.** - [Date](https://rapidrails.cc/docs/date) - `rui_date` - Date/time input with native, picker (JS calendar), and select variants — supports single dates and date ranges **Pro tier.** - [Upload](https://rapidrails.cc/docs/upload) - `rui_upload` - File upload with dropzone/button/minimal variants, preview, accept type presets, and Active Storage direct upload **Pro tier.** ### Navigation & Structure - [Link](https://rapidrails.cc/docs/link) - `rui_link` - Styled link with conditional rendering and Turbo support - [Dropdown](https://rapidrails.cc/docs/dropdown) - `rui_dropdown` - Menu/dropdown for navigation or form selection **Pro tier.** - [Pagination](https://rapidrails.cc/docs/pagination) - `rui_pagination` - Built-in pagination with multiple variants (no external gems needed) - [Steps](https://rapidrails.cc/docs/steps) - `rui_steps` - Multi-step wizard with layout variants, Turbo Frame integration, and built-in step validation - [Tabs](https://rapidrails.cc/docs/tabs) - `rui_tabs` - WAI-ARIA APG tabbed navigation with panels. `:underline`/`:pills`/`:boxed`/`:spaced` variants, horizontal/vertical orientation, deep-linking **Pro tier.** ### Content Display - [Text](https://rapidrails.cc/docs/text) - `rui_text` - Typography with heading, paragraph, blockquote, and inline text - [TextFmt](https://rapidrails.cc/docs/text_fmt) - `rui_text_fmt` - Text with Rails formatting (truncate, numbers, time, linkify) - [Badge](https://rapidrails.cc/docs/badge) - `rui_badge` - Status badges with colors, sizes, shapes, and icons - [Alert](https://rapidrails.cc/docs/alert) - `rui_alert` - Contextual feedback messages with variants and auto-dismiss **Pro tier.** - [Table](https://rapidrails.cc/docs/table) - `rui_table` - Data table with collection mode, formatters, sorting, selection, bulk actions, pagination, responsive modes **Pro tier.** - [CodeBlock](https://rapidrails.cc/docs/code_block) - `rui_code_block` - Syntax-highlighted code snippets with copy button - [Card](https://rapidrails.cc/docs/card) - `rui_card` - Plug-and-play card system with optional icon/header/body/list/media/footer slots, plus a one-line shortcut API (`title:`/`description:`/`icon:`). Exists — do not replace with a plain `
` - [Progress](https://rapidrails.cc/docs/progress) - `rui_progress` - Accessible progress bar for completion/usage/status, with label, value display, and animated transitions - [Stat](https://rapidrails.cc/docs/stat) - `rui_stat` - Metric/KPI card with change indicator, optional progress bar, and icon/footer/sparkline slots. Composes `rui_text`/`rui_badge`/`rui_progress` internally ### Media & Visual - [Image](https://rapidrails.cc/docs/image) - `rui_image` - Semantic HTML image with captions, aspect ratios, and visual effects - [Avatar](https://rapidrails.cc/docs/avatar) - `rui_avatar` - User profile images, initials, or placeholders with status indicators - [Icon](https://rapidrails.cc/docs/icon) - `rui_icon` - 1500+ Lucide icons with color and size control (requires lucide-rails gem) - [SocialButton](https://rapidrails.cc/docs/social_button) - `rui_social_button` - Social platform/payment buttons with branded icons - [Carousel](https://rapidrails.cc/docs/carousel) - `rui_carousel` - Accessible slide carousel (CSS scroll-snap + Stimulus) with 6 variants (`:default`/`:banner`/`:hero`/`:cards`/`:testimonials`/`:fullscreen`) **Pro tier.** ### Interactive & Popovers - [Tooltip](https://rapidrails.cc/docs/tooltip) - `rui_tooltip` - Hover/focus tooltips with multiple position options - [Popover](https://rapidrails.cc/docs/popover) - `rui_popover` - Rich popovers with header, body, footer slots **Pro tier.** - [Dialog](https://rapidrails.cc/docs/dialog) - `rui_dialog` - Native HTML dialog element (modal or drawer) - [Accordion](https://rapidrails.cc/docs/accordion) - `rui_accordion` - Collapsible sections with single or multi-open support **Pro tier.** - [Kanban](https://rapidrails.cc/docs/kanban) - `rui_kanban` - Drag-and-drop kanban board with columns, cards, WIP limits, and server persistence **Pro tier.** ### Real-time & Search - [LiveSearch](https://rapidrails.cc/docs/live_search) - `rui_live_search` - Live search input with debounce and Turbo Frame updates - [Editable](https://rapidrails.cc/docs/editable) - `rui_editable` - Inline editable text with Turbo auto-save - [Clipboard](https://rapidrails.cc/docs/clipboard) - `rui_clipboard` - Copy-to-clipboard button with visual feedback ## Component Selection for Code Generators When building apps from natural language descriptions, use existing RUI components instead of writing custom JavaScript. Every interactive component ships with its own Stimulus controller — **never write a custom Stimulus controller for behavior an RUI component already handles.** ### Composability Patterns — Component Nesting & Slots No single RUI component handles these multi-component layouts — compose from primitives. Generated from `lib/rapid_rails_ui/schema/compositions.yml`. #### Author Byline Avatar + author name + date — used in blog posts, comments, cards Components: `rui_avatar`, `rui_text` Common in: blog posts, comments, article headers, user cards ```erb
<%= rui_avatar(name: user.name, src: user.avatar_url, size: :sm) %>
<%= rui_text(user.name, weight: :medium, inline: true) %> <%= rui_text(time_ago_in_words(post.created_at) + " ago", size: :sm, color: :muted, inline: true) %>
``` Use inline: true on text to prevent

margin. Avatar auto-generates initials from name: if no src:. #### Page Header Page title + description + action buttons — used at top of index/show pages Components: `rui_text`, `rui_button`, `rui_link` Common in: index pages, dashboard headers, settings pages ```erb

<%= rui_text("Posts", as: :h1) %> <%= rui_text("Manage your blog posts", color: :muted) %>
<%= rui_link("New Post", new_post_path, variant: :solid, color: :primary, icon: :plus) %>
``` h1 ignores size: — uses built-in responsive preset. Use rui_link with variant: :solid for button-styled navigation. #### Stat Card Metric label + large value + optional change badge/progress — used in dashboards Components: `rui_stat` Common in: dashboards, admin panels, analytics pages ```erb <%= rui_stat("Total Users", "12,345", change: "+12%", change_color: :success) do |s| %> <% s.with_icon(:users) %> <% end %> ``` rui_stat is a first-class component (v0.51.x+) — do not hand-roll this from rui_text + rui_badge + a plain div. change_direction auto-detects from the +/- prefix on change:. For general (non-metric) card layouts — media, icon+header, body, checklist, footer — use rui_card instead; see its own composition/USAGE.md. #### Empty State Icon + message + CTA button — shown when table/list has no data Components: `rui_icon`, `rui_text`, `rui_link` Common in: empty tables, no search results, first-time user, empty inbox ```erb
<%= rui_icon(:inbox, size: :xl, color: :muted) %> <%= rui_text("No posts yet", as: :h3, class: "mt-4") %> <%= rui_text("Create your first post to get started.", color: :muted) %>
<%= rui_link("Create Post", new_post_path, variant: :solid, color: :primary, icon: :plus) %>
``` Icon uses currentColor — inherits from parent text color. Center with flex column. #### Comment Thread Avatar + author + body + action links — used in discussions, reviews Components: `rui_avatar`, `rui_text`, `rui_link`, `rui_button_to` Common in: comments, discussions, reviews, activity feeds ```erb
<%= rui_avatar(name: comment.user.name, src: comment.user.avatar_url, size: :sm) %>
<%= rui_text(comment.user.name, weight: :medium, inline: true) %> <%= rui_text(time_ago_in_words(comment.created_at) + " ago", size: :sm, color: :muted, inline: true) %>
<%= rui_text(comment.body) %>
<%= rui_link("Reply", "#", size: :sm, color: :muted) %> <%= rui_button_to("Delete", comment_path(comment), method: :delete, size: :xs, color: :danger, variant: :ghost, data: { turbo_confirm: "Delete this comment?" }) %>
``` Use rui_button_to for delete (generates CSRF form). Never nest button_to inside form_with. #### Form Section Section heading + description + grouped form fields — used in settings Components: `rui_text`, `rui_input`, `rui_textarea`, `rui_checkbox` Common in: settings pages, profile forms, multi-section forms ```erb
<%= rui_text("Profile Information", as: :h3) %> <%= rui_text("Update your account details and public profile.", color: :muted, size: :sm) %>
<%= f.rui_input(:first_name, label: "First name") %> <%= f.rui_input(:last_name, label: "Last name") %> <%= f.rui_input(:email, label: "Email", type: :email, class: "sm:col-span-2") %> <%= f.rui_textarea(:bio, label: "Bio", rows: 4, class: "sm:col-span-2") %>
``` Must be inside form_with block. Use f.rui_* form builder methods for fields. h3 ignores size: param. #### Filter Tabs Horizontal pill-style tab bar for filtering resources by status/stage/type Components: `rui_link` Common in: index pages, pipeline views, activity feeds, filtered lists ```erb
<% [["All", nil], ["Active", "active"], ["Archived", "archived"]].each do |label, value| %> <% active = params[:filter] == value %> <%= rui_link(label, resources_path(filter: value), variant: :no_underline, class: "px-4 py-1.5 rounded-md text-sm font-medium transition-colors #{active ? 'bg-white text-gray-950 shadow-sm' : 'text-gray-600 hover:text-gray-900'}") %> <% end %>
``` Uses rui_link (not buttons) for filter navigation. Active tab gets white bg + shadow. Wrap in flex-wrap for many tabs. Pair with controller scope filtering via params. #### Resource List Item Bordered card row with name/link + metadata badges + edit/delete actions Components: `rui_link`, `rui_badge`, `rui_text`, `rui_button_to` Common in: index pages, search results, CRUD listings ```erb
<%= rui_link(resource.name, resource_path(resource), variant: :no_underline, class: "text-sm font-medium text-gray-900 hover:text-gray-700") %> <%= rui_badge(text: resource.status.titleize, color: :secondary, size: :sm, variant: :soft) %>
<%= rui_text(resource.subtitle, as: :span, size: :xs, color: :muted) %>
<%= rui_link("Edit", edit_resource_path(resource), variant: :no_underline, class: "text-sm text-gray-500 hover:text-gray-900") %> <%= rui_button_to("Delete", resource_path(resource), method: :delete, variant: :ghost, color: :danger, size: :sm, turbo_confirm: "Are you sure?") %>
``` Use min-w-0 on flex-1 to enable text truncation. Badge with variant: :soft for subtle status labels. button_to for delete (generates CSRF form) — keep outside any form_with. #### Detail Row Label + value inline pair for key-value display on show pages Components: `rui_text` Common in: show pages, detail cards, profile sections, settings displays ```erb
<%= rui_text("Email:", as: :span, size: :sm, weight: :medium) %> <%= rui_text(resource.email, as: :span, size: :sm) %>
<%= rui_text("Phone:", as: :span, size: :sm, weight: :medium) %> <%= rui_text(resource.phone, as: :span, size: :sm) %>
``` Use as: :span (not default

) to keep label and value on the same line. Wrap each pair in a div. Use space-y-2 on the container for consistent vertical rhythm. #### Action Footer Edit link + delete button separated by a border — used at bottom of show cards Components: `rui_link`, `rui_button_to` Common in: show pages, detail cards, resource detail views ```erb

<%= rui_link("Edit", edit_resource_path(resource), variant: :no_underline, class: "text-sm font-medium text-gray-700 hover:text-gray-900") %> <%= rui_button_to("Delete", resource_path(resource), method: :delete, variant: :ghost, color: :danger, size: :sm, turbo_confirm: "Are you sure?") %>
``` Always place outside any form_with block (button_to generates its own form). Use border-t for visual separation from content above. turbo_confirm: for delete confirmation. #### Associated List Section heading + compact cards for related records — used on show pages Components: `rui_text`, `rui_link`, `rui_badge` Common in: show pages, parent-child relationships, has_many displays ```erb
<%= rui_text("Related Items", as: :p, size: :lg, weight: :bold, class: "mb-4") %> <% if items.any? %>
<% items.each do |item| %>
<%= rui_link(item.name, item_path(item), variant: :no_underline, class: "text-sm font-medium text-gray-900 hover:text-gray-700") %> <%= rui_badge(text: item.status, color: :secondary, size: :sm, variant: :soft) %>
<%= rui_text(item.metadata, as: :span, size: :xs, color: :muted) %>
<% end %>
<% else %>
<%= rui_text("No items yet", size: :sm, color: :muted) %>
<% end %>
``` Uses p-3 (not p-4) for compact rows vs full resource_list_item. Always include an empty state fallback. Section heading uses as: :p (not heading tags) to avoid responsive size presets. ### Disambiguation — Ambiguous Terms These terms commonly cause wrong component selection. Match carefully. Generated from `lib/rapid_rails_ui/schema/families.yml` decision_trees. **What kind of search?** - "Filter table rows" -> `rui_input` — Table has NO built-in search. Use separate input with client-side filtering. NOT `rui_live_search` (Live search is for global/app-wide search, not table filtering) - "Global search / Cmd+K" -> `rui_live_search` — Standalone search with optional modal. Debounced queries via Turbo Frame. Needs search controller endpoint. NOT `rui_input` (Input is for simple forms, not debounced async search) - "Searchable dropdown in form" -> `rui_combobox` — Form field with type-to-filter. Multi-select supported. NOT `rui_select` (Select is native HTML, not searchable); NOT `rui_dropdown` (Dropdown is for action menus, not form fields) **What kind of dropdown?** - "Actions menu / context menu" -> `rui_dropdown` — Clickable menu with items. NOT for form selection. NOT `rui_select` (Select is for form fields, not action menus); NOT `rui_combobox` (Combobox is for searchable form fields) - "Form select field (< 10 options)" -> `rui_select` — Native HTML select. Best for simple lists, mobile UX. NOT `rui_dropdown` (Dropdown is for action menus, not form submission) - "Searchable form select (> 10 options)" -> `rui_combobox` — Searchable select for large option lists. Multi-select supported. NOT `rui_select` (Native select not searchable, bad UX for many options); NOT `rui_dropdown` (Dropdown is for action menus, not form submission) **What kind of popup?** - "Confirmation / form dialog" -> `rui_dialog` — Native HTML element. Use slots for structure. NOT `rui_tooltip` (Tooltip is text-only hover hints, not interactive content); NOT `rui_popover` (Popover is for rich info cards, not confirmations) - "Hover hint" -> `rui_tooltip` — Text-only, small. Triggered on hover/focus. NOT `rui_dialog` (Dialog is for confirmations/forms, not hints); NOT `rui_popover` (Popover is click-triggered with rich content) - "Rich info card on click" -> `rui_popover` — Click-triggered. Supports header/body/footer structure. NOT `rui_tooltip` (Tooltip is hover-triggered text-only); NOT `rui_dialog` (Dialog is modal (blocks interaction), popover is not) **What kind of editing?** - "Click to edit inline" -> `rui_editable` — Click text → input appears → auto-save via PATCH on blur/Enter, cancel on Escape. Needs controller respond_to with format.turbo_stream. NOT `form_with` (form_with is for full forms, not inline editing) - "Edit form page" -> `form_with` — Standard Rails form with f.rui_input, f.rui_textarea, etc. NOT `rui_editable` (Editable is for inline single-field updates, not forms) **What kind of data display?** - "Data table with sorting/selection" -> `rui_table` — No built-in search. Pair with rui_input(type: :search) for filtering. NOT `manual div layout` (Use rui_table for tabular data with features) - "Card grid / simple list" -> `manual HTML layout` — For non-tabular layouts, use manual HTML with RUI components. NOT `rui_table` (Table is for tabular data, not card layouts) **What kind of action?** - "Submit form / primary action" -> `rui_button` — Use inside form_with. Type auto-detects (submit in forms, button standalone). NOT `rui_button_to` (button_to is for RESTful actions outside forms); NOT `rui_link` (Links are for navigation, not form submission) - "Delete / RESTful action" -> `rui_button_to` — RESTful action (POST/PATCH/DELETE). Generates its own form with CSRF. NOT `rui_button` (Use button_to for actions that need HTTP method override); NOT `rui_link` (Links should use GET only (use button_to for other methods)) - "Navigation styled as button" -> `rui_link` — Navigation that looks like a button. Use variant: :solid for button appearance. NOT `rui_button` (Buttons are for actions, links are for navigation); NOT `rui_button_to` (button_to is for form submission, not navigation) **What kind of text display?** - "Page/section heading" -> `rui_text` — Headings use built-in responsive sizes. Do NOT pass size: — it is ignored. Explicit weight: still works. NOT `rui_text with size:` (as: h1-h6 ignores size: param. Use as: alone for headings.) - "Body paragraph / description" -> `rui_text` — Default tag is

with mb-2 margin. Use as: :span or inline: true for inline text - "Formatted text (truncate, numbers, time)" -> `rui_text_fmt` — Use rui_text_fmt for Rails formatting helpers (truncate, number_to_currency, time_ago_in_words, etc.). NOT `rui_text` (rui_text is for typography styling, not Rails formatting helpers) - "Inline text fragment" -> `rui_text` — inline: true renders instead of

. No bottom margin **What kind of profile/user display?** - "User avatar only" -> `rui_avatar` — Renders image or auto-generated initials from name:. Use size: for different sizes - "Avatar with name/info" -> `composition` — No single component for this. Compose avatar + text in a flex container - "Content/hero image" -> `rui_image` — Use rui_image for content images. Supports aspect ratios, effects, captions. NOT `rui_avatar` (Avatar is for user profile images, not content images) **What kind of progress/metric display?** - "Simple progress bar" -> `rui_progress` — Standalone progress bar. Supports label, show_value, animated, 5 sizes, 2 variants. NOT `rui_stat` (Stat is a card for labeled metrics — use rui_progress for a bare progress bar) - "Metric card with optional progress" -> `rui_stat` — Card-style metric display. Optional change badge, progress bar, icon slot, footer slot. Composes rui_text + rui_badge + rui_progress. NOT `rui_progress` (rui_progress is a bare bar — use rui_stat for labeled metric cards) ### Interactive Components with Bundled Stimulus Controllers These components include JavaScript behavior — never rewrite this logic: | Component | Controller | What It Does | |---|---|---| | `rui_editable` | `editable` | Click-to-edit, auto-save PATCH, Enter/Escape/blur | | `rui_live_search` | `live-search` | Debounced search, Turbo Frame results, optional Cmd+K modal | | `rui_dialog` | `dialog` | Modal/drawer open/close, backdrop click, Escape | | `rui_accordion` | `accordion` | Expand/collapse, single or multi-open | | `rui_table` | `table` | Row selection, bulk actions, sortable columns | | `rui_combobox` | `combobox` | Searchable dropdown, keyboard nav | | `rui_clipboard` | `clipboard` | Copy-to-clipboard with feedback | | `rui_upload` | `upload` | Drag-and-drop, file preview, progress | | `rui_steps` | `steps` | Multi-step wizard navigation | | `rui_checkbox` | `checkbox` | Indeterminate state, group select-all | | `rui_date` | `date-picker` | Calendar popup, date range, time | | `rui_kanban` | `kanban` | Drag-and-drop columns/cards, keyboard nav, auto-PATCH | | `rui_carousel` | `carousel` | Scroll-snap slide tracking, auto-rotate, reduced-motion gating | | `rui_tabs` | `tabs` | Tab activation, keyboard nav, deep-link hash sync | ### Controller Setup for Interactive Components Some components send requests to the server. Your Rails controller must handle them: ```erb <%# rui_editable sends a PATCH via fetch — controller must respond: %> format.turbo_stream { head :ok } # success format.json { render json: { errors: @model.errors.full_messages }, status: :unprocessable_entity } # failure ``` ### Common Mistakes — Wrong/Right Examples Generated from `lib/rapid_rails_ui/schema/families.yml` common_mistakes plus renamed-param gotchas from `gotchas.yml`. #### Mistake 1: Using rui_dropdown inside form_with for field selection ```erb <%# WRONG %> <%= form_with model: @post do |f| %> <%= f.rui_dropdown(...) %> <% end %> <%# RIGHT %> <%= form_with model: @post do |f| %> <%= f.rui_select(:category_id, Category.all, :id, :name) %> <% end %> ``` Reason: Dropdown is for action menus (navigation), not form field selection #### Mistake 2: Using rui_live_search to filter table rows ```erb <%# WRONG %> <%= rui_live_search(url: filter_posts_path, ...) %> <%= rui_table(@posts) do |table| %> <%# RIGHT %> <%= rui_input(type: :search, placeholder: 'Filter...', data: { action: 'keyup->filter#search' }) %> <%= rui_table(@posts) do |table| %> ``` Reason: Live search is for global search with backend queries. Table filtering is client-side. #### Mistake 3: Using rui_button for delete actions ```erb <%# WRONG %> <%= rui_button('Delete', data: { turbo_method: :delete, turbo_confirm: 'Sure?' }) %> <%# RIGHT %> <%= rui_button_to('Delete', post_path(@post), method: :delete, data: { turbo_confirm: 'Sure?' }) %> ``` Reason: Buttons need proper forms for non-GET requests. Use button_to for RESTful actions. #### Mistake 4: Setting rui_text with tag: instead of as: ```erb <%# WRONG %> <%= rui_text(tag: :h1) { 'Title' } %> <%# RIGHT %> <%= rui_text(as: :h1) { 'Title' } %> ``` Reason: Text component uses 'as:' parameter, not 'tag:' #### Mistake 5: Using rui_badge with status: instead of color: ```erb <%# WRONG %> <%= rui_badge(text: 'Active', status: :success) %> <%# RIGHT %> <%= rui_badge(text: 'Active', color: :success) %> ``` Reason: Badge deprecated 'status:' in favor of 'color:' for consistency #### Mistake 6: Passing size: with heading tags (h1-h6) ```erb <%# WRONG %> <%= rui_text('Title', as: :h1, size: :sm) %> <%# RIGHT %> <%= rui_text('Title', as: :h1) %> ``` Reason: When as: is h1-h6, size: is silently ignored. Headings use built-in responsive presets. #### Mistake 7: Using leading:/tracking: as primary param names ```erb <%# WRONG %> <%= rui_text('Hello', leading: :tight, tracking: :wide) %> <%# RIGHT %> <%= rui_text('Hello', line_height: :tight, letter_spacing: :wide) %> ``` Reason: leading:/tracking: work as aliases but line_height:/letter_spacing: are canonical param names #### Mistake 8: rui_button_to generates a

— never nest inside form_with ```erb <%# WRONG %> <%= form_with model: @post do |f| %><%= rui_button_to("Delete", post_path(@post), method: :delete) %><% end %> <%# RIGHT %> <%= form_with model: @post do |f| %><%= f.rui_button("Save") %><% end %><%= rui_button_to("Delete", post_path(@post), method: :delete, color: :danger) %> ``` Reason: rui_button_to renders a with CSRF token and method override. Nesting it inside another form_with creates invalid nested forms — browsers ignore the inner form. #### Mistake 9: undo_url: cascades auto_dismiss, animate, and show_progress_bar ```erb <%# WRONG %> <%= rui_alert(type: :success, undo_url: undo_path) { "Deleted" } %> <%# RIGHT %> <%= rui_alert(type: :success, undo_url: undo_path, auto_dismiss: true, animate: true, show_progress_bar: true) { "Deleted" } %> ``` Reason: Passing undo_url: auto-enables auto_dismiss: true, which auto-enables animate: true and show_progress_bar: true. These cascades emit deprecation warnings. Pass all params explicitly to silence them. #### Mistake 10: dismissible: renamed to backdrop_close: — old param emits deprecation warning ```erb <%# WRONG %> <%= rui_dialog(title: "Form", dismissible: false) do |d| %> <%# RIGHT %> <%= rui_dialog(title: "Form", backdrop_close: false) do |d| %> ``` Reason: The dialog param for closing on backdrop click was renamed from dismissible: to backdrop_close: for clarity. Using dismissible: still works but emits a deprecation warning. The Stimulus value was also renamed to backdropCloseValue. #### Mistake 11: max_file_size: renamed to max_file_size_bytes: — old param emits deprecation warning ```erb <%# WRONG %> <%= rui_upload(:avatar, max_file_size: 2.megabytes) %> <%# RIGHT %> <%= rui_upload(:avatar, max_file_size_bytes: 2.megabytes) %> ``` Reason: The upload file size limit param was renamed to max_file_size_bytes: to be explicit about the unit. Old param emits deprecation warning. #### Mistake 12: with_footer(href:) silently degrades to plain styled text (no ) when the card itself has href: — never a nested link ```erb <%# WRONG %> <%= rui_card(href: lot_path(@lot)) do |c| %><% c.with_footer(text: "View details", href: lot_path(@lot)) %><% end %> <%# RIGHT %> <%= rui_card(href: lot_path(@lot)) do |c| %><% c.with_footer(text: "View details") %><% end %> ``` Reason: When rui_card's own href: is set, the whole card renders as a single . A footer slot's own href: is passed wrapped_in_link: true internally and the footer renders as styled text instead of a second — nesting inside is invalid HTML and unusable for assistive tech. No warning is raised; the footer's href: is simply ignored for linking purposes (the arrow icon and styling still render). #### Mistake 13: panels: false renders the tablist only — matching an external panel's id to the built-in pattern does nothing automatically ```erb <%# WRONG %> <%= rui_tabs(id: "wizard", panels: false) do |t| %><% t.with_tab(id: "step-1", label: "Step 1") %><% end %>
Step 1 content
<%# RIGHT %> <%= rui_tabs(id: "wizard", panels: false) do |t| %><% t.with_tab(id: "step-1", label: "Step 1") %><% end %>
Step 1 content
``` Reason: With panels: false, the component doesn't emit aria-controls on the tab triggers at all, so the rui--tabs Stimulus controller has no panel id to look up and never toggles anything for you — regardless of what id an external panel uses, even if it exactly matches the built-in "#{id}-panel-#{tab_id}" convention. You must listen for the tab-change event the controller dispatches on every activation (rui--tabs:change, or tabs:change if config.stimulus_namespace is unset) and toggle your own panels from its detail.tab. #### Mistake 14: show_arrows: true is silently overridden — arrows/indicators never render with fewer than 2 slides, no matter what show_arrows:/show_indicators: say ```erb <%# WRONG %> <%= rui_carousel(id: "promo", show_arrows: true) do |c| %><% c.with_slide { image_tag("banner.jpg") } %><% end %> <%# RIGHT %> <%# Arrows only appear once a 2nd with_slide call is added — this is not a bug to work around with show_arrows:, it is the intended UX (no point showing prev/next with nothing to navigate to) %> ``` Reason: The component.html.erb template gates every arrow/indicator render on `slides.size > 1` in addition to @show_arrows/@show_indicators — this check is unconditional and cannot be overridden by passing show_arrows: true or show_indicators: true explicitly. A single-slide carousel (or one that starts with only one with_slide call, e.g. while content is loading) renders with zero arrows/indicators regardless of what you passed. #### Mistake 15: max_cards: on a full column rejects the drop with zero user feedback — no error event, no shake animation, no message ```erb <%# WRONG %> <%= rui_kanban do |board| %><% board.with_column(title: "WIP", id: "wip", max_cards: 3) do |col| %>...<% end %><% end %> <%# RIGHT %> <%# Pair max_cards: with a visible affordance — e.g. badge_count: showing "3/3" so the limit is visible before a user attempts the drag %><% board.with_column(title: "WIP", id: "wip", max_cards: 3, badge_count: current_wip_count) do |col| %> ``` Reason: kanban_controller.js checks the target column's data-max-cards against its current card count on both dragover and drop; when the column is full it does a bare `return` and the drop is silently rejected — no kanban:move-failed event fires (that event is reserved for SERVER-side rejections after a move_url PATCH), no visual feedback, nothing in the console. A user dragging a card to a full column just sees it snap back with no explanation. #### Mistake 16: value: silently coerces nil/non-numeric input to 0% via #to_i — no error, so an upstream calculation bug renders as an empty bar instead of failing loudly ```erb <%# WRONG %> <%= rui_progress(@deal.completion_percentage) %> <%# renders 0% silently if @deal.completion_percentage is nil due to a missing association %> <%# RIGHT %> <%= rui_progress(@deal.completion_percentage || raise("completion_percentage missing for deal #{@deal.id}")) %> <%# fail loudly in dev/staging instead of shipping a silently-wrong 0% bar %> ``` Reason: clamp_value calls val.to_i.clamp(0, 100). Ruby's #to_i on nil returns 0, and on a non-numeric String (e.g. "N/A", "", a broken interpolation) also returns 0 with no exception. A value: that should have been a real percentage but came from a nil association or a failed calculation renders as a plain empty 0% bar — visually indistinguishable from a legitimately-zero metric. ## Common Usage Patterns ### Simple Button ```erb <%= rui_button("Click Me") %> <%= rui_button("Submit", color: :primary, size: :lg) %> <%= rui_button("Delete", color: :danger, variant: :outline) %> ``` ### Form with Inputs ```erb <%= form_with(model: @post) do |f| %> <%= f.rui_input(:title, label: "Title") %> <%= f.rui_textarea(:body, label: "Body", rows: 8, auto_resize: true) %> <%= f.rui_checkbox(:published, label: "Publish") %> <%= f.rui_button %> <% end %> ``` ### Navigation ```erb
<%= rui_link("Home", root_path, unless_current: true) %> <%= rui_link("Posts", posts_path, unless_current: true) %>
``` ### Table with Selection ```erb <%= rui_table(selectable: true, sortable: true) do |table| %> <% table.with_column(key: :name, label: "Name", sortable: true) %> <% table.with_column(key: :email, label: "Email") %> <% @users.each do |user| %> <% table.with_row(id: user.id) do |row| %> <% row.with_cell { user.name } %> <% row.with_cell { user.email } %> <% end %> <% end %> <% end %> ``` ### Alert from Flash ```erb
<%= rui_flash %>
``` ### Inline Editable Field ```erb <%= rui_editable @post, :title, url: post_path(@post), as: :h1, class: "text-2xl font-bold" %> ``` ### Dashboard Stat Row ```erb
<%= rui_stat("Total Users", "12,847", change: "+12%", change_color: :emerald, progress: 72) do |s| %> <% s.with_icon(:users) %> <% end %>
``` ### Card Grid ```erb
<% @sales.each do |sale| %> <%= rui_card(variant: :elevated, color: :accent) do |c| %> <% c.with_icon(name: "clock") %> <% c.with_header(title: sale.title) %> <% c.with_body(text: sale.description) %> <% c.with_footer(text: "View sale", href: sale_path(sale)) %> <% end %> <% end %>
``` ## Built-in Pagination (No External Gems Needed) RapidRailsUI includes its own pagination service. No need to install Pagy, Kaminari, or any other pagination gem. The `rui_paginate` helper is automatically available in all your controllers with zero configuration. ```ruby # In your controller — nothing to install, include, or configure: class PostsController < ApplicationController def index @pagy, @posts = rui_paginate(Post.all, limit: 10) end end ``` ```erb <%= rui_pagination(pagy: @pagy) %> <%= rui_table(pagy: @pagy) do |table| ... end %> ``` **Features:** works with ActiveRecord relations and plain Arrays; handles page clamping, empty collections, grouped queries; accepts `limit:` and `page_param:`. ## Critical Constraints & Gotchas 1. **Argument order follows Rails**: `rui_link(text, url)` just like Rails `link_to(text, url)` - text first 2. **Use `rui_button_to` for destructive actions**: Not `rui_link` with method: option 3. **Icons require lucide-rails**: Install the gem to use `rui_icon` 4. **Form methods are on the form builder**: Use `f.rui_checkbox` in forms, not standalone 5. **Dark mode is automatic**: Don't add manual `dark:` classes to components 6. **Symbols not strings for options**: Use `color: :primary` not `color: "primary"` 7. **Form builder auto-labels buttons**: `f.rui_button` generates "Create Post" or "Update Post" automatically 8. **Turbo integration for real-time**: Components like LiveSearch, Editable, Dialog work best with Turbo Frames ### Per-Component Parameter Gotchas Non-obvious param behaviors that cause silent failures or unexpected output — generated from `lib/rapid_rails_ui/schema/gotchas.yml`. Full detail (wrong/right examples, detection regex, recovery steps) is in that file; this is the AI-facing summary. **High severity:** - **rui_text**: as: h1-h6 silently ignores size: param (but weight/leading/tracking work) - **rui_button_to**: rui_button_to generates a — never nest inside form_with - **rui_dialog**: dismissible: renamed to backdrop_close: — old param emits deprecation warning - **rui_tabs**: panels: false renders the tablist only — matching an external panel's id to the built-in pattern does nothing automatically - **rui_stat**: change_direction: is computed, stored, and tested but does not currently drive any rendered output — no up/down arrow icon exists in the template **Medium severity:** - **rui_text**: Default

tag adds mb-2 bottom margin - **rui_alert**: undo_url: cascades auto_dismiss, animate, and show_progress_bar - **rui_upload**: max_file_size: renamed to max_file_size_bytes: — old param emits deprecation warning - **rui_card**: with_footer(href:) silently degrades to plain styled text (no ) when the card itself has href: — never a nested link - **rui_carousel**: show_arrows: true is silently overridden — arrows/indicators never render with fewer than 2 slides, no matter what show_arrows:/show_indicators: say - **rui_kanban**: max_cards: on a full column rejects the drop with zero user feedback — no error event, no shake animation, no message **Low severity:** - **rui_text**: Explicit weight: DOES work with heading tags (asymmetric with size:) - **rui_text**: leading:/tracking: are aliases — leading takes priority over line_height - **rui_progress**: value: silently coerces nil/non-numeric input to 0% via #to_i — no error, so an upstream calculation bug renders as an empty bar instead of failing loudly ## Component Output Reference | Component | Tag | Display | Key Behavior | |---|---|---|---| | `rui_text` | `

` default | block (span=inline) | `

` adds mb-2; h1-h6 ignore size: | | `rui_text_fmt` | `

` default | block (span=inline) | Same as rui_text + formatting (truncate, highlight, currency, time_ago) | | `rui_avatar` | ``/`` | inline | name: auto-generates initials | | `rui_badge` | ``/`` | inline | Safe inside text/table cells | | `rui_button` | `