# 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 32 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. ## 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` - button component - `rui_link` - link component - `rui_checkbox` - checkbox component - etc. ### Argument Order Follows Rails Conventions RapidRailsUI follows standard Rails conventions for argument order: ```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 RapidRailsUI uses semantic colors AND Tailwind colors: ```erb <%= rui_button("Submit", color: :primary) %> <%= rui_badge(text: "Success", color: :success) %> <%= rui_alert(type: :danger) %> <%= rui_button("Action", color: :indigo) %> <%= rui_badge(text: "Tag", color: :amber) %> ``` Available semantic colors: `:primary`, `:secondary`, `:tertiary`, `:accent`, `:success`, `:danger`, `:warning`, `:info`, `:muted`, `:default` Available Tailwind colors: `:zinc`, `:gray`, `:red`, `:orange`, `:amber`, `:blue`, `:green`, `:indigo`, `:violet`, `:pink`, and many more. ### Form Builder Integration Use form builder methods within `form_with` blocks: ```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 %> <% end %> ``` ### Dark Mode All components automatically support dark mode via Tailwind's `dark:` variants. No manual dark mode styling needed. ```erb <%= rui_button("Click me") %> <%= rui_alert(type: :info) { "Dark-mode ready content" } %> ``` ## 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. Works like Rails `f.select`: `f.rui_select(:status, ["Draft", "Published"])` or `f.rui_select(:author_id, Author.all, :id, :name)`. Also accepts keyword style: `f.rui_select(:field, collection: [...], label: "X")`. - [Combobox](https://rapidrails.cc/docs/combobox) - `rui_combobox` - Searchable dropdown with single/multi-select, collection config, and keyboard navigation - [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 - [Upload](https://rapidrails.cc/docs/upload) - `rui_upload` - File upload with dropzone/button/minimal variants, preview, accept type presets, and Active Storage direct upload ### Navigation & Structure - [Link](https://rapidrails.cc/docs/link) - `rui_link` - Styled link with conditional rendering and Turbo support. **Note:** `email:`, `phone:`, `sms:`, `method: non-GET` params are deprecated — use dedicated helpers below. - [Mailto](https://rapidrails.cc/docs/link#mailto) - `rui_mailto(text, address)` - Email link with optional subject/body/cc - [Tel](https://rapidrails.cc/docs/link#tel) - `rui_tel(text, number)` - Phone link with optional country code - [SMS](https://rapidrails.cc/docs/link#sms) - `rui_sms(text, number)` - SMS link with optional pre-filled body - [Dropdown](https://rapidrails.cc/docs/dropdown) - `rui_dropdown` - Menu/dropdown for navigation or form selection - [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 7 layout variants, Turbo Frame integration, and built-in step validation ### 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. **Note:** `status:` param is deprecated — use `color:` instead. - [Alert](https://rapidrails.cc/docs/alert) - `rui_alert` - Contextual feedback messages with variants and auto-dismiss. Supports `color:` override. **Cascade note:** `undo_url:` auto-enables `auto_dismiss:`, which auto-enables `animate:` and `show_progress_bar:` — pass these explicitly to avoid deprecation warnings. - [Table](https://rapidrails.cc/docs/table) - `rui_table` - Data table with collection mode, 10 built-in formatters, sorting, selection, bulk actions, pagination, and responsive modes - [CodeBlock](https://rapidrails.cc/docs/code_block) - `rui_code_block` - Syntax-highlighted code snippets with copy button ### 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 ### 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 - [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 - [Kanban](https://rapidrails.cc/docs/kanban) - `rui_kanban` - Drag-and-drop kanban board with columns, cards, WIP limits, and server persistence ### 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 ### Flash Messages - [FlashContainer](https://rapidrails.cc/docs/alert#flash-container) - `rui_flash_container` - Container for Turbo flash messages - [Flash Helper](https://rapidrails.cc/docs/alert#flash-helper) - `rui_flash` - Smart flash message rendering - [Errors Helper](https://rapidrails.cc/docs/alert#errors-helper) - `rui_errors` - Model validation error display - [AlertTurbo](https://rapidrails.cc/docs/alert#turbo-alert) - `rui_alert_turbo` - Single alert via Turbo Stream ## 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 Many RUI components support **composition** via ViewComponent slots. Slots let you nest components inside each other for structured layouts. #### Dialog Composition (Modal & Drawer) Dialog uses `position:` to switch between **modal** (`:center`) and **drawer** (`:right`, `:left`, `:top`, `:bottom`). 4 slots: `trigger`, `header`, `body`, `footer`. **Params:** `title:`, `size:` (`:sm`, `:md`, `:lg`, `:xl`, `:2xl`, `:full`), `position:` (`:center` modal, `:right/:left/:top/:bottom` drawer), `backdrop:` (`:default`, `:dark`, `:blur`, `:light`), `closable:` (show X button), `backdrop_close:` (close on backdrop click, replaces deprecated `dismissible:`), `static_backdrop:` (prevent Escape close), `open:` (start open), `turbo_frame:` (Turbo Frame ID for dynamic loading), `autofocus_selector:` (CSS selector to focus on open) ```erb <%# Modal (centered) %> <%= rui_dialog(title: "Confirm Action", size: :md, position: :center, backdrop: :blur) do |dialog| %> <% dialog.with_trigger { rui_button("Open Dialog", color: :primary) } %> <% dialog.with_body do %>

This action cannot be undone.

<% end %> <% dialog.with_footer do %> <%= rui_button("Cancel", variant: :outline, data: { action: "click->dialog#close" }) %> <%= rui_button_to("Confirm", action_path, method: :delete, color: :danger) %> <% end %> <% end %> <%# Drawer (slides from right) %> <%= rui_dialog(title: "Settings", size: :lg, position: :right) do |dialog| %> <% dialog.with_trigger { rui_button("Open Drawer") } %> <% dialog.with_body do %> <%= render "settings_form" %> <% end %> <% end %> <%# Turbo Frame — load content lazily %> <%= rui_dialog(title: "User Details", turbo_frame: "user-details") do |dialog| %> <% dialog.with_trigger { rui_link("View", "#") } %> <% dialog.with_body do %> Loading... <% end %> <% end %> ``` **Common mistakes:** - ❌ WRONG: Putting content directly in dialog block without slots → Content won't render - ❌ WRONG: Using `dialog.header` instead of `dialog.with_header` → Slot not recognized - ❌ WRONG: Using `dismissible: false` → Deprecated, use `backdrop_close: false` - ❌ WRONG: Using `position: "right"` (string) → Must be symbol `:right` - ✅ RIGHT: Use `position: :center` for modals, `position: :right` for drawers, always use `with_*` slots #### Dropdown Composition (Action Menus) Dropdown has 13 slots (items, headers, dividers, search, footer, etc.). Most common pattern: ```erb <%= rui_dropdown do |dropdown| %> <% dropdown.with_trigger { rui_button("Actions", icon: :chevron_down) } %> <% dropdown.with_header(text: "User Actions") %> <% dropdown.with_item(text: "View Profile", href: profile_path, icon: :user) %> <% dropdown.with_item(text: "Edit Settings", href: settings_path, icon: :settings) %> <% dropdown.with_divider %> <% dropdown.with_item(text: "Sign Out", href: signout_path, icon: :log_out, color: :danger) %> <% end %> ``` **Common mistakes:** - ❌ WRONG: Using `rui_dropdown` inside `form_with` for field selection → Use `rui_select` instead - ❌ WRONG: Manually building dropdown HTML → Use slot methods - ✅ RIGHT: Use `with_item` for each menu option, `with_divider` for separators #### Table Composition (Data Tables) Table has **two modes**: Manual Mode (full control) and Collection Mode (auto-renders from data). 7 slots: `column`, `row`, `toolbar`, `empty_state`, `bulk_actions`, `footer`, `caption`. **Params:** `striped:`, `hoverable:`, `bordered:`, `headless:`, `sticky_header:`, `sticky_max_height:`, `sortable:`, `sort_column:`, `sort_direction:`, `sort_url:`, `selectable:` (row checkboxes), `responsive_mode:` (`:scroll`, `:cards`, `:stack`), `pagy:` (pagination), `turbo_frame:`, `loading:` (skeleton state), `title:`, `description:` **Column params:** `table.with_column(key:, label:, sortable:, align:, width:, formatter:)` **10 built-in formatters:** `:date`, `:datetime`, `:time`, `:relative_time`, `:currency`, `:number`, `:percentage`, `:boolean`, `:truncate`, `:badge` ```erb <%# Manual Mode — full control over each cell %> <%= rui_table(striped: true, hoverable: true) do |table| %> <% table.with_column(key: :name, label: "Name", sortable: true) %> <% table.with_column(key: :email, label: "Email") %> <% table.with_column(key: :joined, label: "Joined", formatter: :relative_time) %> <% table.with_column(key: :actions, label: "Actions", align: :right) %> <% @users.each do |user| %> <% table.with_row(id: user.id) do |row| %> <% row.with_cell { user.name } %> <% row.with_cell { user.email } %> <% row.with_cell { user.created_at } %> <% row.with_cell do %> <%= rui_link("Edit", edit_user_path(user), icon: :edit) %> <% end %> <% end %> <% end %> <% end %> <%# With toolbar, bulk actions, and pagination %> <%= rui_table(selectable: true, pagy: @pagy, sort_url: users_path) do |table| %> <% table.with_toolbar do %> <%= rui_input(:search, placeholder: "Search users...", size: :sm) %> <% end %> <% table.with_bulk_actions do %> <%= rui_bulk_action "Delete Selected", url: bulk_destroy_users_path, method: :delete, confirm: "Delete selected users?", color: :danger %> <% end %> <% table.with_column(key: :name, label: "Name", sortable: true) %> <% table.with_column(key: :role, label: "Role") %> <% @users.each do |user| %> <% table.with_row(id: user.id) do |row| %> <% row.with_cell { user.name } %> <% row.with_cell { rui_badge(text: user.role, color: :primary) } %> <% end %> <% end %> <% table.with_empty_state do %>

No users found. Try a different search.

<% end %> <% end %> ``` **Critical pattern:** Column-cell binding is **POSITIONAL** — column 1 maps to cell 1 by order. If you add/remove columns, add/remove cells in same order. **Common mistakes:** - ❌ WRONG: Column count doesn't match cell count → Data under wrong headers - ❌ WRONG: Forgetting `@users.each` loop → No rows render - ❌ WRONG: Using `formatter: "date"` (string) → Must be symbol `:date` - ❌ WRONG: Building custom bulk-delete forms → Use `rui_bulk_action` helper inside `with_bulk_actions` slot - ✅ RIGHT: Define columns, loop records, match cell count to column count. Use `rui_bulk_action` for bulk operations. #### Accordion Composition (Collapsible Sections) Accordion uses `with_item` slot for each panel. Supports icons, subtitles, Turbo Frame lazy-loading, and chevron customization. **Params:** `exclusive:` (only one open at a time, default true), `variant:` (`:default`, `:bordered`, `:separated`, `:flush`, `:ghost`), `dividers:` (show separators), `show_chevron:`, `chevron_position:` (`:left`, `:right`), `animate:`, `color:`, `size:`, `shape:`, `spacing:` **Item params:** `with_item(title:, icon:, icon_trailing:, subtitle:, expanded:, disabled:, collapsible:, src:, loading:)` ```erb <%= rui_accordion(exclusive: true, variant: :bordered, dividers: true) do |accordion| %> <% accordion.with_item(title: "What is RapidRailsUI?", icon: :info) do %>

A production-ready ViewComponent UI library for Rails with 32 components.

<% end %> <% accordion.with_item(title: "Installation", subtitle: "Quick setup guide", expanded: true) do %>

Add to Gemfile: gem 'rapid_rails_ui'. Run bundle install.

<% end %> <% accordion.with_item(title: "Dark Mode", disabled: true) do %>

Coming soon!

<% end %> <%# Turbo Frame lazy-load — content fetched on expand %> <% accordion.with_item(title: "Advanced Config", src: advanced_config_path, loading: :lazy) do %>

Loading...

<% end %> <% end %> ``` **Common mistakes:** - ❌ WRONG: Using `accordion.item` instead of `accordion.with_item` → Slot not recognized - ❌ WRONG: Using `open: true` → Correct param is `expanded: true` - ❌ WRONG: Manually fetching lazy content with JS → Use `src:` param for Turbo Frame lazy-loading - ✅ RIGHT: Use `with_item(title:)` for each section, `expanded:` to start open, `src:` for lazy content #### Kanban Composition (Drag-and-Drop Board) Kanban uses nested slots: `board.with_column` → `col.with_card`. Cards accept any block content. ```erb <%= rui_kanban(move_url: kanban_move_path) do |board| %> <% board.with_column(title: "To Do", id: "todo", color: :zinc) do |col| %> <% col.with_card(id: "card-1", color: :danger) do %>
<%= rui_badge(text: "Bug", color: :danger, size: :sm) %>

Fix login bug

<% end %> <% col.with_footer do %> <% end %> <% end %> <% board.with_column(title: "In Progress", id: "wip", max_cards: 3, badge_count: 2) do |col| %> <% col.with_card(id: "card-2") do %>

Design dashboard

<% end %> <% end %> <% end %> ``` **Common mistakes:** - ❌ WRONG: Missing `id:` on columns or cards → Drag-and-drop breaks - ❌ WRONG: Writing custom drag-and-drop JS → Use `rui_kanban` with `move_url:` for auto-PATCH - ✅ RIGHT: Use `board.with_column(id:, title:)` for columns, `col.with_card(id:)` for cards #### Steps Composition (Multi-step Wizard) Steps requires a **Turbo Frame wrapper** for smooth transitions. The `turbo_frame_id` param must match the wrapping `` element's `id`. This is the #1 mistake AI agents make with this component. **Params:** `id:` (required), `url:` (required), `current_step:` (0-based index), `turbo_frame_id:` (matches outer frame), `variant:` (`:horizontal`, `:vertical`, `:minimal`, `:progress`, `:breadcrumb`, `:timeline`, `:vertical_cards`), `navigation:` (`:linear`, `:free`, `:completed_only`), `error_step:` (step index with server error), `cache_locally:` (LocalStorage caching, default true), `client_validation:` (HTML5 validation, default true), `back_text:`, `next_text:`, `submit_text:`, `loading_text:` **Slots:** `with_step(title:, description:, icon:, optional:)`, `with_header`, `with_footer` ```erb <%# CRITICAL: Wrap in turbo-frame — id MUST match turbo_frame_id param %> <% frame_id = "registration-frame" %> <%= rui_steps( id: "registration-wizard", url: wizards_path, current_step: @current_step, turbo_frame_id: frame_id, variant: :horizontal, navigation: :linear ) do |steps| %> <% steps.with_header do %>

Create Your Account

<% end %> <% steps.with_step(title: "Account") do %> <%= render "account_fields" %> <% end %> <% steps.with_step(title: "Profile", description: "Optional info", optional: true) do %> <%= render "profile_fields" %> <% end %> <% steps.with_step(title: "Confirm", icon: :check) do %> <%= render "confirmation" %> <% end %> <% end %>
``` **Controller pattern:** ```ruby class WizardsController < ApplicationController def show @current_step = session[:wizard_step].to_i end def update # Save step data, advance step session[:wizard_step] = params[:current_step].to_i + 1 @current_step = session[:wizard_step] # Server-side validation error? Set error_step: if @errors_on_step_1 @error_step = 1 end redirect_to wizard_path end end ``` **Common mistakes:** - ❌ WRONG: `rui_steps` without `` wrapper → Step transitions do full page reloads or break with Turbo Frame mismatch error - ❌ WRONG: `turbo_frame_id` doesn't match the outer `` → Content disappears on step change - ❌ WRONG: Using `variant: "horizontal"` (string) → Must be symbol `:horizontal` - ✅ RIGHT: Always wrap in ``, pass matching `turbo_frame_id:`, use symbol variants #### Combobox Composition (Searchable Dropdown) Combobox is a Stimulus-powered searchable dropdown supporting single and multi-select. Use instead of `rui_select` when you need type-to-filter, icons in options, or multi-select with badges. **Params:** `method:` (required), `collection:` (required — array, hash, `[text, value]` pairs, or AR relation), `label:`, `placeholder:`, `help_text:`, `multiple:` (multi-select), `max_selections:`, `show_badges:` (multi-select badges), `selected:`, `searchable:` (default true), `close_on_select:` (`:auto` = true for single, false for multi), `clear_on_select:`, `variant:` (`:default`, `:underline`), `size:`, `color:`, `shape:`, `width:`, `disabled:`, `required:` **Collection config:** `value_method:` (default `:id`), `text_method:` (default `:name`), `icon_method:` — use these when passing AR relations or complex objects. ```erb <%# Single-select with AR relation %> <%= form_with(model: @post) do |f| %> <%= f.rui_combobox(:author_id, collection: Author.all, value_method: :id, text_method: :name, label: "Author", placeholder: "Search authors..." ) %> <% end %> <%# Multi-select with badges %> <%= form_with(model: @post) do |f| %> <%= f.rui_combobox(:tag_ids, collection: Tag.all, value_method: :id, text_method: :name, multiple: true, max_selections: 5, show_badges: true, label: "Tags" ) %> <% end %> <%# Standalone (outside form builder) %> <%= rui_combobox(:country, collection: [["United States", "us"], ["Canada", "ca"], ["Mexico", "mx"]], placeholder: "Select country" ) %> ``` **Common mistakes:** - ❌ WRONG: Using `rui_select` for searchable dropdowns → Use `rui_combobox` for type-to-filter - ❌ WRONG: Passing AR relation without `value_method:`/`text_method:` → Defaults to `:id`/`:name`, will error if model doesn't have `:name` - ❌ WRONG: Using `multiple: true` with `close_on_select: true` → Dropdown closes after each pick, frustrating UX - ✅ RIGHT: Use `close_on_select: :auto` (default) — auto-closes for single, stays open for multi #### Upload Composition (File Upload) Upload supports three variants: `:dropzone` (drag-and-drop area), `:button` (file picker button), `:minimal` (compact). Includes preview, validation, and Active Storage direct upload. **Params:** `name_or_method:` (required), `variant:` (`:dropzone`, `:button`, `:minimal`), `accept:` (string `'image/png'`, symbol `:image`/`:video`/`:audio`/`:pdf`/`:document`/`:spreadsheet`/`:text`/`:archive`, or array), `max_file_size_bytes:` (default 10MB), `multiple:`, `max_files:`, `preview:` (show preview), `preview_size:`, `remove_button:`, `label:`, `help_text:`, `dropzone_text:`, `button_text:`, `direct_upload:` (Active Storage), `color:`, `size:`, `shape:`, `required:`, `disabled:` ```erb <%# Dropzone with image-only and size limit %> <%= form_with(model: @post) do |f| %> <%= f.rui_upload(:cover_image, variant: :dropzone, accept: :image, max_file_size_bytes: 5.megabytes, preview: true, dropzone_text: "Drag cover image here", label: "Cover Image" ) %> <% end %> <%# Multiple file upload with Active Storage direct upload %> <%= form_with(model: @post) do |f| %> <%= f.rui_upload(:attachments, multiple: true, max_files: 10, accept: [:image, :pdf, :document], direct_upload: true, label: "Attachments" ) %> <% end %> <%# Compact button variant %> <%= form_with(model: @user) do |f| %> <%= f.rui_upload(:avatar, variant: :button, accept: :image, button_text: "Choose Avatar") %> <% end %> ``` **Common mistakes:** - ❌ WRONG: Using `max_file_size:` → Deprecated, use `max_file_size_bytes:` - ❌ WRONG: Using `accept: "image"` (string) for preset → Use symbol `accept: :image` for presets, or string `accept: "image/png"` for MIME types - ❌ WRONG: Using `multiple: true` without `max_files:` → Users can upload unlimited files - ✅ RIGHT: Use symbol presets for `accept:`, always set `max_file_size_bytes:` and `max_files:` limits #### Date Composition (Date Picker & Range) Date supports three variants: `:native` (browser input), `:picker` (JS calendar popup), `:select` (3 dropdowns). The `:picker` variant supports **range mode** for selecting date ranges. **Params:** `variant:` (`:native`, `:picker`, `:select`), `input_type:` (`:date`, `:datetime`, `:time`), `range:` (Boolean, enables date range), `min:`, `max:`, `unavailable:` (Array of dates to block), `value:` (Date or `{start:, end:}` Hash for range). Picker extras: `months:` (calendars shown), `presets:` (quick-select buttons), `with_today:`, `clearable:`, `week_numbers:`, `selectable_header:` (month/year dropdowns), `start_day:` (0=Sun, 1=Mon), `separator:`. ```erb <%# Single date with picker %> <%= form_with(model: @event) do |f| %> <%= f.rui_date(:start_date, variant: :picker, label: "Event Date", min: Date.today, with_today: true, clearable: true ) %> <% end %> <%# Date range picker (e.g., booking dates) %> <%= form_with(model: @booking) do |f| %> <%= f.rui_date(:dates, variant: :picker, range: true, months: 2, min: Date.today, unavailable: @blocked_dates, presets: true, label: "Stay Dates" ) %> <% end %> <%# Select variant (for birthdate) %> <%= form_with(model: @user) do |f| %> <%= f.rui_date(:birthdate, variant: :select, order: [:month, :day, :year], start_year: 1920, end_year: Date.today.year, include_blank: "Select..." ) %> <% end %> ``` **Common mistakes:** - ❌ WRONG: Using `type: :date` → Deprecated, use `input_type: :date` - ❌ WRONG: Using `range: true` with `variant: :native` → Range only works with `:picker` variant - ❌ WRONG: Passing `value: "2024-01-15"` (string) for range → Use Hash: `value: { start: Date.new(2024,1,15), end: Date.new(2024,1,20) }` - ✅ RIGHT: Use `variant: :picker` with `range: true` for date ranges, pass `unavailable:` array to block dates #### Live Search Composition (Search with Turbo) Live Search requires **two mandatory params**: `url:` (search endpoint) and `turbo_frame:` (target frame for results). It sends debounced GET requests and renders results inside a Turbo Frame. **Params:** `url:` (required), `turbo_frame:` (required), `placeholder:`, `debounce:` (ms, default 300), `min_length:` (default 1), `param_name:` (default `:q`), `size:`, `shape:`, `clear_button:`, `loading_indicator:`, `search_button:`, `shortcut:` (e.g. `"k"` for Cmd+K), `voice_search:`, `modal:` (opens in dialog), `modal_size:`, `scope:` (Array of `{value:, label:}` hashes for scope dropdown), `scope_param:`, `recent_searches:`, `recent_searches_limit:`, `recent_searches_key:` **Slots:** `with_empty_state { }`, `with_trigger { }` (modal mode) ```erb <%# Basic search with Turbo Frame results %> <%= rui_live_search( url: search_posts_path, turbo_frame: "search-results", placeholder: "Search posts...", clear_button: true, loading_indicator: true, shortcut: "k" ) %> <%= render @posts %> <%# With scope dropdown and recent searches %> <%= rui_live_search( url: search_path, turbo_frame: "results", scope: [ { value: "posts", label: "Posts" }, { value: "users", label: "Users" }, { value: "comments", label: "Comments" } ], scope_default: "posts", recent_searches: true, recent_searches_limit: 10 ) do |search| %> <% search.with_empty_state do %>

No results found.

<% end %> <% end %> ``` **Controller pattern:** ```ruby class SearchController < ApplicationController def index @posts = Post.where("title ILIKE ?", "%#{params[:q]}%") # Respond with turbo_frame matching the search component's turbo_frame: end end ``` **Common mistakes:** - ❌ WRONG: Missing `turbo_frame:` param → Results won't render (Turbo can't find target frame) - ❌ WRONG: `` doesn't match `turbo_frame:` value → Content disappears - ❌ WRONG: Controller responds with full page layout → Wrap results view in `` - ✅ RIGHT: Always match `turbo_frame:` with `` in both the search component and the results view #### Icon Slots (Buttons, Links, Badges) Many components accept icon slots for visual enhancement: ```erb <%= rui_button("Save Changes", color: :primary) do |button| %> <% button.with_icon(:save) %> <% end %> <%= rui_button("Save", icon: :save, color: :primary) %> <%= rui_link("Documentation", docs_path) do |link| %> <% link.with_icon(:book_open) %> <% end %> <%= rui_badge(text: "Active") do |badge| %> <% badge.with_icon(:check_circle) %> <% end %> ``` **Common mistakes:** - ❌ WRONG: Using string `"heart"` instead of symbol `:heart` → Icon not found - ❌ WRONG: Mixing `icon:` param AND `with_icon` slot → Slot overrides param - ✅ RIGHT: Use symbol for icon name, choose either param OR slot (not both) ### Disambiguation — Ambiguous Terms These terms commonly cause wrong component selection. Match carefully: **"Search":** - "Search/filter a table" / "filter rows" → `rui_input` with `type: :search` placed above a `rui_table`. The table has NO built-in search — use a separate input. **NOT** `rui_live_search`. - "Global search" / "Cmd+K" / "search popup" / "search the whole app" → `rui_live_search` (standalone search with optional modal, debounced queries via Turbo Frame — needs a search controller endpoint). - "Searchable dropdown" / "type to filter options" → `rui_combobox` (form field with type-to-filter). **"Dropdown":** - "Actions menu" / "context menu" / "navigation dropdown" → `rui_dropdown` (clickable menu with items). - "Form select field" / "pick one option" → `rui_select` (native HTML select) or `rui_combobox` (searchable). **"Editable":** - "Click to edit" / "inline edit" / "edit in place" → `rui_editable` (click text → input appears → auto-save via PATCH on blur/Enter, cancel on Escape). Needs controller `respond_to` with `format.turbo_stream { head :ok }`. - "Edit form" / "form page" → Standard `form_with` with `f.rui_input`, `f.rui_textarea`, etc. **"Modal" / "Popup":** - "Confirmation dialog" / "modal form" / "drawer panel" → `rui_dialog` (native HTML dialog element). - "Small hover hint" → `rui_tooltip` (text-only, hover/focus triggered). - "Rich info card on click" → `rui_popover` (header/body/footer, click triggered). **"Table":** - "Data table" / "sortable list" / "table with selection" → `rui_table` (sorting, row selection, bulk actions, pagination via `pagy:`). No built-in search — pair with `rui_input(type: :search)` for filtering. - "Card grid" / "simple list" → Manual HTML layout with RUI components inside. **"Button":** - "Submit form" / "primary action" → `rui_button` (inside a form, or standalone). - "Delete" / "POST/PATCH/DELETE action" → `rui_button_to` (RESTful action, generates its own form). - "Navigation that looks like a button" → `rui_link` with button-like styling. ### 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 | ### 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 These are the most frequent mistakes when generating RUI code: #### Mistake 1: Using rui_dropdown for form field selection ```erb <%= form_with model: @post do |f| %> <%= f.rui_dropdown(:category_id, Category.all, :id, :name) %> <% end %> <%= form_with model: @post do |f| %> <%= f.rui_select(:category_id, Category.all, :id, :name) %> <% end %> ``` **Why:** `rui_dropdown` is a navigation/action menu component. Use `rui_select` (native HTML select) or `rui_combobox` (searchable) for form fields. #### Mistake 2: Using rui_live_search to filter table rows ```erb <%= rui_live_search(url: filter_posts_path, turbo_frame: "posts") %> <%= rui_table(@posts) do |table| %> <%# ... %> <% end %> <%= rui_input(type: :search, placeholder: "Filter posts...", data: { action: "keyup->filter#search" }) %> <%= rui_table(@posts, data: { filter_target: "table" }) do |table| %> <%# ... %> <% end %> ``` **Why:** `rui_live_search` sends debounced queries to the server (for global app search). Table filtering is typically client-side using a simple input. #### Mistake 3: Using rui_button for delete actions ```erb <%= rui_button("Delete", data: { turbo_method: :delete, turbo_confirm: "Sure?" }) %> <%= rui_button_to("Delete", post_path(@post), method: :delete, data: { turbo_confirm: "Sure?" }, color: :danger) %> ``` **Why:** `rui_button_to` generates a proper form with CSRF token and HTTP method override. Plain `rui_button` is for in-form submissions or JavaScript actions. #### Mistake 4: Wrong parameter names (deprecated/renamed) ```erb <%= rui_text(tag: :h1) { "Title" } %> <%= rui_badge(text: "Active", status: :success) %> <%= rui_text(as: :h1) { "Title" } %> <%= rui_badge(text: "Active", color: :success) %> <%= rui_text("Hello", line_height: :tight, letter_spacing: :wide) %> ``` **Why:** `rui_text` uses `as:` not `tag:`. `rui_badge` deprecated `status:` in favor of `color:`. Use `line_height:` and `letter_spacing:` for text spacing control. #### Mistake 5: Mismatched column/cell count in tables ```erb <%= rui_table(striped: true) do |table| %> <% table.with_column(key: :name, label: "Name") %> <% table.with_column(key: :email, label: "Email") %> <% table.with_column(key: :role, label: "Role") %> <% @users.each do |user| %> <% table.with_row do |row| %> <% row.with_cell { user.name } %> <% row.with_cell { user.email } %> <% end %> <% end %> <% end %> <%= rui_table(striped: true) do |table| %> <% table.with_column(key: :name, label: "Name") %> <% table.with_column(key: :email, label: "Email") %> <% table.with_column(key: :role, label: "Role") %> <% @users.each do |user| %> <% table.with_row do |row| %> <% row.with_cell { user.name } %> <% row.with_cell { user.email } %> <% row.with_cell { rui_badge(text: user.role, color: :primary) } %> <% end %> <% end %> <% end %> ``` **Why:** Table column-cell binding is **positional**. Column 1 maps to cell 1, column 2 to cell 2, etc. Mismatch causes data under wrong headers. #### Mistake 6: Using string instead of symbol for icons ```erb <%= rui_button("Save", icon: "heart", color: :primary) %> <%= rui_button("Save", icon: :heart, color: :primary) %> ``` **Why:** Lucide icon lookup requires symbol keys. String keys won't find icons. #### Mistake 7: Putting content directly in component blocks without slots ```erb <%= rui_dialog(title: "Confirm") do %>

Are you sure?

<%= rui_button("OK") %> <% end %> <%= rui_dialog(title: "Confirm") do |dialog| %> <% dialog.with_body do %>

Are you sure?

<% end %> <% dialog.with_footer do %> <%= rui_button("OK", color: :primary) %> <% end %> <% end %> ``` **Why:** Components with slots (Dialog, Dropdown, Accordion) require explicit slot methods. Direct content in the block is ignored. #### Mistake 8: Expecting size: to work with heading tags ```erb <%# WRONG: size: is ignored when as: is h1-h6 %> <%= rui_text("Title", as: :h1, size: :xl) %> <%# Renders text-5xl md:text-6xl (h1 preset), NOT text-xl %> <%# RIGHT: Headings have built-in responsive sizes %> <%= rui_text("Title", as: :h1) %> <%# RIGHT: For custom-sized heading-like text, skip as: %> <%= rui_text("Small Title", size: :lg, weight: :bold) %> ``` **Why:** When `as:` is h1-h6, `size:` is completely ignored — headings use TypographyHelper responsive presets. Explicit `weight:` DOES override the heading default. #### Mistake 9: Deprecated dialog backdrop param ```erb <%# WRONG: dismissible: is deprecated %> <%= rui_dialog(title: "Confirm", dismissible: false) do |d| %> Content <% end %> <%# RIGHT: Use backdrop_close: %> <%= rui_dialog(title: "Confirm", backdrop_close: false) do |d| %> Content <% end %> ``` **Why:** `dismissible:` was renamed to `backdrop_close:` to distinguish it from `closable:` (the X button). Using the old name emits a deprecation warning. #### Mistake 10: Deprecated upload file size param ```erb <%# WRONG: max_file_size: is deprecated %> <%= rui_upload(:avatar, max_file_size: 2.megabytes) %> <%# RIGHT: Use max_file_size_bytes: %> <%= rui_upload(:avatar, max_file_size_bytes: 2.megabytes) %> ``` **Why:** Renamed to make the unit explicit (bytes). ## 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) %> <%= rui_link("Settings", settings_path, unless_current: true) %>
``` ### Social Media Links with Icons ```erb
<%= rui_link(url: "https://twitter.com/username", color: :primary, external: true, title: "Twitter") do |link| %> <% link.with_icon(:twitter) %> <% end %> <%= rui_link(url: "https://github.com/username", color: :primary, external: true, title: "GitHub") do |link| %> <% link.with_icon(:github) %> <% end %> <%= rui_link(url: "https://linkedin.com/company/yourcompany", color: :primary, external: true, title: "LinkedIn") do |link| %> <% link.with_icon(:linkedin) %> <% end %>
``` **Key points for social media links:** - Use `url:` as keyword argument (not positional) when using block syntax - `external: true` adds security attributes (`rel="noopener noreferrer"` and `target="_blank"`) - `title:` attribute improves accessibility for icon-only links - Block syntax with `link.with_icon()` required for icons - Icons inherit link color unless custom `color:` specified on icon ### 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") %> <% table.with_bulk_actions do %> <%= rui_bulk_action("Delete", url: bulk_destroy_path, method: :delete, color: :danger) %> <% end %> <% @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 %>
``` Then in your controller: ```ruby redirect_to posts_path, notice: "Post created successfully" ``` ### Inline Editable Field ```erb <%= rui_editable @post, :title, url: post_path(@post), as: :h1, class: "text-2xl font-bold" %> ``` ### Multi-step Wizard ```erb <%# IMPORTANT: turbo-frame wrapper required for Turbo integration %> <% frame_id = "registration-frame" %> <%= rui_steps( id: "registration", url: registration_path, current_step: @step, turbo_frame_id: frame_id, variant: :horizontal, navigation: :linear ) do |steps| %> <% steps.with_step(title: "Account Info") do %> <%= render "account_form" %> <% end %> <% steps.with_step(title: "Profile") do %> <%= render "profile_form" %> <% end %> <% steps.with_step(title: "Confirm") do %> <%= render "confirmation" %> <% 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 <%# In your view: %> <%= rui_pagination(pagy: @pagy) %> <%# Or with table (shows "Showing X-Y of Z" in footer): %> <%= rui_table(pagy: @pagy) do |table| ... end %> ``` **Features:** - Works with ActiveRecord relations and plain Arrays - Handles edge cases: page clamping, empty collections, grouped queries - Accepts `limit:` (items per page) and `page_param:` (query param name, defaults to `:page`) ## 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 ## 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` | `