# RapidRailsUI - Complete Documentation > Production-ready ViewComponent UI library for Ruby on Rails with Tailwind CSS, Hotwire, and Stimulus JS. This is the complete, detailed documentation file for RapidRailsUI. For a more concise version optimized for AI context windows, see [llms.txt](llms.txt). **Components**: 31 **Last Updated**: 2026-01-17 at 14:59 EST --- ## Table of Contents 1. [Accordion](#accordion) 2. [Alert](#alert) 3. [Avatar](#avatar) 4. [Badge](#badge) 5. [Button](#button) 6. [Button To](#button-to) 7. [Checkbox](#checkbox) 8. [Clipboard](#clipboard) 9. [Code Block](#code-block) 10. [Combobox](#combobox) 11. [Date](#date) 12. [Dialog](#dialog) 13. [Dropdown](#dropdown) 14. [Editable](#editable) 15. [Icon](#icon) 16. [Image](#image) 17. [Input](#input) 18. [Link](#link) 19. [Live Search](#live-search) 20. [Pagination](#pagination) 21. [Popover](#popover) 22. [Radio Button](#radio-button) 23. [Select](#select) 24. [Social Button](#social-button) 25. [Steps](#steps) 26. [Table](#table) 27. [Text](#text) 28. [Text Fmt](#text-fmt) 29. [Textarea](#textarea) 30. [Tooltip](#tooltip) 31. [Upload](#upload) --- ## Accordion # Accordion Component The Accordion component creates collapsible content sections, perfect for FAQs, settings panels, navigation menus, and organizing content into expandable sections. ## Basic Usage ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "What is RapidRailsUI?") do %> RapidRailsUI is a ViewComponent-based UI library for Rails with full Tailwind CSS integration. <% end %> <% accordion.with_item(title: "How do I install it?") do %> Add the gem to your Gemfile and run the install generator. <% end %> <% accordion.with_item(title: "Is it free?") do %> RapidRailsUI requires a commercial license for production use. <% end %> <% end %> ``` ## Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `color` | Symbol | `:zinc` | Color theme (`:zinc`, `:gray`, `:slate`, `:primary`, `:secondary`, `:success`, `:warning`, `:danger`, `:info`) | | `size` | Symbol | `:base` | Size (`:xs`, `:sm`, `:base`, `:lg`, `:xl`) | | `variant` | Symbol | `:default` | Visual style (`:default`, `:bordered`, `:separated`, `:flush`, `:ghost`) | | `shape` | Symbol | `:rounded` | Border radius (`:none`, `:rounded`, `:square`, `:pill`) | | `spacing` | Symbol | `:base` | Space between items (`:none`, `:xs`, `:sm`, `:base`, `:lg`, `:xl`) - used when `dividers: false` or `variant: :separated` | | `exclusive` | Boolean | `true` | Only allow one item open at a time | | `animate` | Boolean | `true` | Enable smooth expand/collapse animations | | `expand_icon` | Symbol | `:chevron_down` | Icon for collapsed state | | `collapse_icon` | Symbol | `nil` | Icon for expanded state (if nil, expand_icon rotates 180°) | | `show_chevron` | Boolean | `true` | Show/hide the expand/collapse icon | | `chevron_position` | Symbol | `:right` | Position of chevron (`:left`, `:right`) | | `dividers` | Boolean | `true` | Show dividers between items | ### Item Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `title` | String | **required** | Header text for the item | | `icon` | Symbol | `nil` | Lucide icon name for the header (leading) | | `icon_trailing` | Symbol | `nil` | Lucide icon name for trailing position | | `subtitle` | String | `nil` | Secondary text below the title | | `expanded` | Boolean | `false` | Start with item expanded | | `disabled` | Boolean | `false` | Disable the item (cannot be toggled) | | `collapsible` | Boolean | `true` | Whether item can be collapsed (false = always open) | | `src` | String | `nil` | URL for Turbo Frame lazy loading | | `loading` | Symbol | `:lazy` | Loading strategy for Turbo Frame (`:lazy`, `:eager`) | ## Variants ### Default Standard accordion with borders and background. ```erb <%= rui_accordion(variant: :default) do |accordion| %> <% accordion.with_item(title: "Section 1") { "Content 1" } %> <% accordion.with_item(title: "Section 2") { "Content 2" } %> <% end %> ``` ### Bordered Thicker borders for more visual emphasis. ```erb <%= rui_accordion(variant: :bordered) do |accordion| %> <% accordion.with_item(title: "Section 1") { "Content 1" } %> <% accordion.with_item(title: "Section 2") { "Content 2" } %> <% end %> ``` ### Separated Card-style items with shadows and spacing. ```erb <%= rui_accordion(variant: :separated, spacing: :lg) do |accordion| %> <% accordion.with_item(title: "Card 1") { "Content 1" } %> <% accordion.with_item(title: "Card 2") { "Content 2" } %> <% end %> ``` ### Flush Minimal style with only bottom borders, no rounded corners. ```erb <%= rui_accordion(variant: :flush) do |accordion| %> <% accordion.with_item(title: "Section 1") { "Content 1" } %> <% accordion.with_item(title: "Section 2") { "Content 2" } %> <% end %> ``` ### Ghost No borders or background, just the content. ```erb <%= rui_accordion(variant: :ghost) do |accordion| %> <% accordion.with_item(title: "Section 1") { "Content 1" } %> <% accordion.with_item(title: "Section 2") { "Content 2" } %> <% end %> ``` ## Colors ### Semantic Colors ```erb <%# Primary - Blue %> <%= rui_accordion(color: :primary) do |accordion| %> <% accordion.with_item(title: "Primary Section") { "Content" } %> <% end %> <%# Success - Green %> <%= rui_accordion(color: :success) do |accordion| %> <% accordion.with_item(title: "Success Section") { "Content" } %> <% end %> <%# Warning - Amber %> <%= rui_accordion(color: :warning) do |accordion| %> <% accordion.with_item(title: "Warning Section") { "Content" } %> <% end %> <%# Danger - Red %> <%= rui_accordion(color: :danger) do |accordion| %> <% accordion.with_item(title: "Danger Section") { "Content" } %> <% end %> <%# Info - Sky %> <%= rui_accordion(color: :info) do |accordion| %> <% accordion.with_item(title: "Info Section") { "Content" } %> <% end %> ``` ### Neutral Colors ```erb <%= rui_accordion(color: :zinc) do |accordion| %> <% accordion.with_item(title: "Zinc Section") { "Content" } %> <% end %> <%= rui_accordion(color: :gray) do |accordion| %> <% accordion.with_item(title: "Gray Section") { "Content" } %> <% end %> <%= rui_accordion(color: :slate) do |accordion| %> <% accordion.with_item(title: "Slate Section") { "Content" } %> <% end %> ``` ## Sizes ```erb <%# Extra Small %> <%= rui_accordion(size: :xs) do |accordion| %> <% accordion.with_item(title: "XS Item") { "Compact content" } %> <% end %> <%# Small %> <%= rui_accordion(size: :sm) do |accordion| %> <% accordion.with_item(title: "Small Item") { "Small content" } %> <% end %> <%# Base (Default) %> <%= rui_accordion(size: :base) do |accordion| %> <% accordion.with_item(title: "Base Item") { "Standard content" } %> <% end %> <%# Large %> <%= rui_accordion(size: :lg) do |accordion| %> <% accordion.with_item(title: "Large Item") { "Larger content" } %> <% end %> <%# Extra Large %> <%= rui_accordion(size: :xl) do |accordion| %> <% accordion.with_item(title: "XL Item") { "Extra large content" } %> <% end %> ``` ## Icons Add icons to accordion headers for better visual context. ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "Profile Settings", icon: :user) do %> Manage your profile information, avatar, and display name. <% end %> <% accordion.with_item(title: "Security", icon: :shield) do %> Update your password and enable two-factor authentication. <% end %> <% accordion.with_item(title: "Notifications", icon: :bell) do %> Configure email and push notification preferences. <% end %> <% accordion.with_item(title: "Billing", icon: :credit_card) do %> View invoices and manage payment methods. <% end %> <% end %> ``` ### Trailing Icons Add icons after the title for status indicators or badges. ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "Pro Feature", icon: :star, icon_trailing: :lock) do %> Upgrade to Pro to unlock this feature. <% end %> <% accordion.with_item(title: "Completed", icon: :check_circle, icon_trailing: :badge_check) do %> This task has been completed. <% end %> <% end %> ``` ## Subtitles Add secondary text below the title for additional context. ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "Account Settings", subtitle: "Manage your profile and preferences", icon: :user) do %> Configure your account details, change email, and update your password. <% end %> <% accordion.with_item(title: "Billing", subtitle: "View invoices and payment methods", icon: :credit_card) do %> Manage your subscription, update payment details, and download invoices. <% end %> <% accordion.with_item(title: "Notifications", subtitle: "Control what alerts you receive", icon: :bell) do %> Choose which notifications to receive via email, SMS, or push. <% end %> <% end %> ``` ## Chevron Position Control where the expand/collapse indicator appears. ### Right Position (Default) ```erb <%= rui_accordion(chevron_position: :right) do |accordion| %> <% accordion.with_item(title: "Chevron on Right") { "Content" } %> <% end %> ``` ### Left Position ```erb <%= rui_accordion(chevron_position: :left) do |accordion| %> <% accordion.with_item(title: "Chevron on Left") { "Content" } %> <% end %> ``` ## Custom Expand/Collapse Icons Use custom icons instead of the default chevron. ### Plus/Minus Style ```erb <%= rui_accordion(expand_icon: :plus, collapse_icon: :minus) do |accordion| %> <% accordion.with_item(title: "Click to Expand") { "Content revealed!" } %> <% end %> ``` ### Circle Plus/Minus Style ```erb <%= rui_accordion(expand_icon: :circle_plus, collapse_icon: :circle_minus) do |accordion| %> <% accordion.with_item(title: "Toggle Content") { "Hidden content here." } %> <% end %> ``` ### Rotating Icon (Default Behavior) When `collapse_icon` is not set, the `expand_icon` rotates 180° when expanded. ```erb <%= rui_accordion(expand_icon: :chevron_down) do |accordion| %> <% accordion.with_item(title: "Rotating Chevron") { "Content" } %> <% end %> ``` ## Hide Chevron Remove the expand/collapse icon entirely. ```erb <%= rui_accordion(show_chevron: false) do |accordion| %> <% accordion.with_item(title: "No Chevron", icon: :info) do %> Click anywhere on the header to toggle. <% end %> <% end %> ``` ## Dividers Control the visual separator between items. ### With Dividers (Default) ```erb <%= rui_accordion(dividers: true) do |accordion| %> <% accordion.with_item(title: "Section 1") { "Content 1" } %> <% accordion.with_item(title: "Section 2") { "Content 2" } %> <% end %> ``` ### Without Dividers (Uses Spacing) ```erb <%= rui_accordion(dividers: false, spacing: :sm) do |accordion| %> <% accordion.with_item(title: "Section 1") { "Content 1" } %> <% accordion.with_item(title: "Section 2") { "Content 2" } %> <% end %> ``` ## Always-Open Items (Non-Collapsible) Create items that cannot be collapsed - useful for static headers or pinned content. ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "Important Notice", collapsible: false, icon: :alert_circle) do %> This section is always visible and cannot be collapsed. <% end %> <% accordion.with_item(title: "Collapsible Section") do %> This section can be toggled open and closed. <% end %> <% end %> ``` ## Turbo Frame Support (Lazy Loading) Load content on-demand using Turbo Frames for better performance. ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "Comments", src: comments_path) do %> <%# Loading placeholder is shown automatically %> <% end %> <% accordion.with_item(title: "Activity Log", src: activity_log_path, loading: :lazy) do %> <%# Content loads when expanded %> <% end %> <% accordion.with_item(title: "Preloaded Data", src: data_path, loading: :eager, expanded: true) do %> <%# Content loads immediately because expanded: true %> <% end %> <% end %> ``` ### Server-Side Setup ```ruby # app/controllers/comments_controller.rb def index @comments = @post.comments render partial: "comments/list", locals: { comments: @comments } end ``` ```erb <%# app/views/comments/_list.html.erb %> ``` ## Expanded State Set items to be expanded by default. ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "Expanded by Default", expanded: true) do %> This section is open when the page loads. <% end %> <% accordion.with_item(title: "Collapsed") do %> This section starts closed. <% end %> <% end %> ``` ## Multiple Open Items Allow multiple items to be open simultaneously by setting `exclusive: false`. ```erb <%= rui_accordion(exclusive: false) do |accordion| %> <% accordion.with_item(title: "Section 1", expanded: true) do %> This can be open... <% end %> <% accordion.with_item(title: "Section 2", expanded: true) do %> ...at the same time as this! <% end %> <% accordion.with_item(title: "Section 3") do %> And this one too. <% end %> <% end %> ``` ## Disabled Items Prevent specific items from being toggled. ```erb <%= rui_accordion do |accordion| %> <% accordion.with_item(title: "Active Section") do %> This section can be toggled. <% end %> <% accordion.with_item(title: "Disabled Section", disabled: true) do %> This section cannot be opened. <% end %> <% end %> ``` ## Without Animation Disable smooth transitions for instant expand/collapse. ```erb <%= rui_accordion(animate: false) do |accordion| %> <% accordion.with_item(title: "Instant Toggle") do %> No animation delay. <% end %> <% end %> ``` ## Shapes ### Rounded (Default) ```erb <%= rui_accordion(shape: :rounded) do |accordion| %> <% accordion.with_item(title: "Rounded Corners") { "Content" } %> <% end %> ``` ### Square ```erb <%= rui_accordion(shape: :square) do |accordion| %> <% accordion.with_item(title: "Sharp Corners") { "Content" } %> <% end %> ``` ### Pill ```erb <%= rui_accordion(shape: :pill) do |accordion| %> <% accordion.with_item(title: "Extra Rounded") { "Content" } %> <% end %> ``` ## Spacing Control the gap between accordion items (used when `dividers: false` or `variant: :separated`). ```erb <%# No spacing %> <%= rui_accordion(spacing: :none, dividers: false) do |accordion| %> <% accordion.with_item(title: "Item 1") { "Content" } %> <% accordion.with_item(title: "Item 2") { "Content" } %> <% end %> <%# Large spacing %> <%= rui_accordion(spacing: :lg, dividers: false) do |accordion| %> <% accordion.with_item(title: "Item 1") { "Content" } %> <% accordion.with_item(title: "Item 2") { "Content" } %> <% end %> ``` ## Real-World Examples ### FAQ Section ```erb
<%= rui_text("Frequently Asked Questions", as: :h2, size: :2xl, weight: :bold, class: "mb-6") %> <%= rui_accordion(color: :primary, variant: :separated, spacing: :base) do |accordion| %> <% accordion.with_item(title: "How do I get started?", icon: :rocket, subtitle: "Quick setup guide") do %> <%= rui_text("Getting started is easy:", class: "mb-2") %>
  1. <%= rui_text("Add the gem to your Gemfile") %>
  2. <%= rui_text("Run bundle install") %>
  3. <%= rui_text("Run rails g rapid_rails_ui:install") %>
  4. <%= rui_text("Start using components in your views") %>
<% end %> <% accordion.with_item(title: "What Rails versions are supported?", icon: :layers, subtitle: "Compatibility info") do %> <%= rui_text("RapidRailsUI supports Rails 7.0 and above, with full compatibility for Rails 7.1 and 7.2. We recommend using the latest stable Rails release.") %> <% end %> <% accordion.with_item(title: "Is Tailwind CSS required?", icon: :palette, subtitle: "Dependencies") do %> <%= rui_text("Yes, RapidRailsUI is built on Tailwind CSS v4. The install generator will set up Tailwind automatically if it's not already configured.") %> <% end %> <% accordion.with_item(title: "Can I customize the components?", icon: :settings, subtitle: "Customization options") do %> <%= rui_text("Absolutely! You can customize components in several ways:", class: "mb-2") %> <% end %> <% end %>
``` ### Settings Panel ```erb <%= rui_accordion(exclusive: false, variant: :bordered) do |accordion| %> <% accordion.with_item(title: "Account Settings", subtitle: "Manage your profile", icon: :user, expanded: true) do %>
<%= rui_text("Display Name", size: :sm, weight: :medium) %>
<%= rui_text("Email", size: :sm, weight: :medium) %>
<% end %> <% accordion.with_item(title: "Privacy", subtitle: "Control your visibility", icon: :eye_off) do %>
<% end %> <% accordion.with_item(title: "Notifications", subtitle: "Email and push alerts", icon: :bell) do %>
<% end %> <% end %> ``` ### Product Details with Lazy Loading ```erb <%= rui_accordion(variant: :flush, color: :zinc) do |accordion| %> <% accordion.with_item(title: "Description", expanded: true) do %> <%= rui_text("Premium quality product crafted with attention to detail. Made from sustainable materials and designed to last.") %> <% end %> <% accordion.with_item(title: "Specifications") do %>
Material
100% Organic Cotton
Weight
180 GSM
Care
Machine wash cold
<% end %> <% accordion.with_item(title: "Shipping & Returns") do %> <% end %> <%# Lazy load reviews %> <% accordion.with_item(title: "Reviews (24)", src: product_reviews_path(@product), icon: :star) do %> <%# Loading spinner shown automatically %> <% end %> <% end %> ``` ### Navigation Menu with Non-Collapsible Header ```erb ``` ### Team Directory with Avatars ```erb <%= rui_accordion(variant: :separated, spacing: :sm, exclusive: false) do |accordion| %> <% @teams.each do |team| %> <% accordion.with_item( title: team.name, subtitle: "#{team.members.count} members", icon: :users ) do %>
<% team.members.each do |member| %>
<%= rui_avatar(src: member.avatar_url, alt: member.name, size: :sm) %>
<%= rui_text(member.name, weight: :medium) %> <%= rui_text(member.role, size: :sm, color: :muted) %>
<%= rui_badge(member.status, color: member.active? ? :success : :secondary, size: :sm) %>
<% end %>
<% end %> <% end %> <% end %> ``` ## Stimulus Controller The accordion uses a Stimulus controller for interactivity. Make sure to register it: ```javascript // app/javascript/controllers/index.js import AccordionController from "rapid_rails_ui/accordion/accordion_controller" application.register("accordion", AccordionController) ``` ### JavaScript API The controller exposes methods for programmatic control: ```javascript // Get the controller const accordion = document.querySelector('[data-controller="accordion"]') const controller = application.getControllerForElementAndIdentifier(accordion, 'accordion') // Open item by index controller.open(0) // Close item by index controller.close(0) // Toggle item by index controller.toggleItem(1) // Expand all (only works when exclusive: false) controller.expandAll() // Collapse all controller.collapseAll() ``` ### Custom Events The accordion dispatches events you can listen to: ```javascript document.addEventListener('accordion:expanded', (event) => { console.log('Item expanded:', event.detail.index) }) document.addEventListener('accordion:collapsed', (event) => { console.log('Item collapsed:', event.detail.index) }) ``` ## Accessibility The accordion follows WAI-ARIA best practices: - Uses `role="region"` for the accordion container - Header buttons have `aria-expanded` and `aria-controls` attributes - Content regions have `aria-labelledby` pointing to their headers - Non-collapsible items use `
` instead of ` ``` ### Color System Architecture The color system is organized into three layers: 1. **Color Palettes** (`app/assets/stylesheets/rapidrailsui/utilities/colors-palettes.css`) - Defines all available colors (Tailwind colors + custom palettes) - Example: `--color-boulder-500`, `--color-blue-700` 2. **Semantic Mappings** (`app/assets/stylesheets/rapidrailsui/utilities/colors-semantic.css`) - Maps semantic names to actual colors based on configuration - Example: `--color-primary-500: var(--color-boulder-500)` - Auto-generated by `rails rapidrailsui:update_colors` 3. **Component Colors** (`app/assets/stylesheets/rapidrailsui/utilities/colors-components.css`) - UI-specific colors for backgrounds, borders, etc. ### Changing Colors To change your app's colors: 1. **Edit configuration** in `config/initializers/rapidrailsui.rb`: ```ruby config.colors = { primary: 'blue', # Change from default secondary: 'rose', # Change from default accent: 'amber' # Change from default } ``` 2. **Run the rake task**: ```bash rails rapidrailsui:update_colors ``` 3. **Restart your server** and all components will use the new colors! ### Available Color Palettes **RapidCascade (RC) Colors:** slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose **Custom Brand Palettes:** boulder, tradewind, punch, light-orchid, dust-storm, butterfly-bush --- ## Accessibility - Uses appropriate ARIA roles and keyboard navigation - Automatically handles aria-busy for loading states - Proper disabled state handling with aria-disabled - External links include proper security attributes --- ## Customization - You can extend or override styles using the `classes` option. - Customize colors by defining CSS variables in your theme. - All styles are customizable by defining CSS variables. --- ## See Also - [Button Styles](./styles.rb) - [Button Component](./component.rb) - [CSS Variables](../../assets/stylesheets/rapidrailsui/utilities/colors.css) --- For more examples, see the component previews and test files. --- ## Button To # RapidRailsUI ButtonTo Component Usage The `ButtonTo` component renders a `
``` This allows: - RESTful DELETE/PATCH/PUT via POST forms (Rails method spoofing) - Single-click actions without writing form boilerplate - CSRF protection - Turbo/AJAX support --- ## See Also - [Button Component](../button/USAGE.md) - For `
<%# The dialog itself %> <%= rui_dialog(id: "confirm-delete", title: "Confirm Delete") do |d| %> <% d.with_body do %> Are you sure? <% end %> <% d.with_footer do %> <%= rui_button("Cancel", data: { action: "dialog#close" }) %> <%= rui_button("Delete", color: :danger) %> <% end %> <% end %> ``` ### Pre-Opened Dialog ```erb <%= rui_dialog(id: "welcome", title: "Welcome!", open: true) do %> Welcome to our application! <% end %> ``` ## Backdrop Options ### Default Backdrop ```erb <%= rui_dialog(backdrop: :default, title: "Default") do %> Standard semi-transparent backdrop. <% end %> ``` ### Dark Backdrop ```erb <%= rui_dialog(backdrop: :dark, title: "Dark Backdrop") do %> Darker backdrop for more focus. <% end %> ``` ### Blur Backdrop ```erb <%= rui_dialog(backdrop: :blur, title: "Blur Backdrop") do %> Blurred backdrop effect. <% end %> ``` ### Light Backdrop ```erb <%= rui_dialog(backdrop: :light, title: "Light Backdrop") do %> Lighter, more subtle backdrop. <% end %> ``` ## Behavior Options ### Non-Dismissible Dialog Prevent closing when clicking the backdrop: ```erb <%= rui_dialog(dismissible: false, title: "Complete This Form") do %>

Please fill out all required fields before closing.

<%= render "required_form" %> <% end %> ``` ### Static Backdrop (Prevent Escape) Prevent closing with the Escape key (not recommended for most cases): ```erb <%= rui_dialog(static_backdrop: true, title: "Critical Action") do %>

This requires your explicit confirmation.

<% end %> ``` ### No Close Button Hide the X button in the header: ```erb <%= rui_dialog(closable: false, title: "Required Action") do |d| %> <% d.with_body do %> Complete this action to continue. <% end %> <% d.with_footer do %> <%= rui_button("Complete", data: { action: "dialog#close" }) %> <% end %> <% end %> ``` ## Custom Header Replace the default header with custom content: ```erb <%= rui_dialog do |d| %> <% d.with_header do %>
<%= rui_avatar(src: @user.avatar_url, size: :sm) %>

<%= @user.name %>

Edit Profile

<% end %> <% d.with_body do %> <%= render "profile_form", user: @user %> <% end %> <% end %> ``` ## Real-World Examples ### Confirmation Dialog ```erb <%= rui_dialog(id: "confirm-action", title: "Confirm Action", size: :sm) do |d| %> <% d.with_body do %>

Are you sure you want to proceed? This action cannot be undone.

<% end %> <% d.with_footer do %> <%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %> <%= rui_button("Confirm", color: :primary) %> <% end %> <% end %> ``` ### Settings Drawer ```erb <%= rui_dialog(position: :right, size: :lg, title: "Settings") do |d| %> <% d.with_body do %>

Appearance

Notifications

<% end %> <% d.with_footer do %> <%= rui_button("Save Changes", color: :primary) %> <% end %> <% end %> ``` ### Image Preview Modal ```erb <%= rui_dialog(size: :xl, closable: true) do %> <%= @image.alt %> <% end %> ``` ### Mobile Navigation Drawer ```erb <%= rui_dialog(position: :left, size: :md, title: "Menu") do |d| %> <% d.with_body do %> <% end %> <% end %> ``` ### Form in Dialog with Turbo ```erb <%# app/views/posts/new.html.erb %> <%= rui_dialog(title: "New Post", turbo_frame: "dialog") do |d| %> <% d.with_body do %> <%= form_with model: @post do |f| %>
<%= f.label :title, class: "block font-medium mb-1" %> <%= f.text_field :title, class: "w-full rounded border px-3 py-2" %>
<%= f.label :content, class: "block font-medium mb-1" %> <%= f.text_area :content, class: "w-full rounded border px-3 py-2", rows: 4 %>
<% end %> <% end %> <% d.with_footer do %> <%= rui_button("Cancel", variant: :outline, data: { action: "dialog#close" }) %> <%= rui_button("Create Post", color: :primary, type: :submit, form: "new_post") %> <% end %> <% end %> ``` ## Stimulus Controller The dialog uses a Stimulus controller for JavaScript interactivity. The controller is automatically registered when using the component. ### JavaScript API ```javascript // Get the controller const wrapper = document.querySelector('[data-controller="dialog"]') const controller = application.getControllerForElementAndIdentifier(wrapper, 'dialog') // Open the dialog controller.open() // Close the dialog controller.close() // Toggle open/close controller.toggle() ``` ### Custom Events The dialog dispatches custom events: ```javascript document.addEventListener('dialog:open', (event) => { console.log('Dialog opened:', event.detail.dialog) }) document.addEventListener('dialog:closed', (event) => { console.log('Dialog closed:', event.detail.returnValue) }) document.addEventListener('dialog:backdropClick', (event) => { console.log('Backdrop was clicked') }) document.addEventListener('dialog:cancel', (event) => { console.log('Escape key was pressed') }) ``` ### Data Attributes | Attribute | Description | |-----------|-------------| | `data-dialog-dismissible-value` | Whether backdrop click closes dialog | | `data-dialog-position-value` | Position of the dialog | | `data-dialog-static-backdrop-value` | Whether Escape is prevented | | `data-dialog-autofocus-value` | CSS selector for autofocus element | ## Accessibility The Dialog component leverages native `` element accessibility features: - **Focus Trapping**: `showModal()` automatically traps focus inside the dialog - **Escape Key**: Native handling to close the dialog (can be prevented with `static_backdrop`) - **ARIA Attributes**: `aria-modal="true"` and `aria-labelledby` are set automatically - **Inert Background**: The rest of the page becomes inert when dialog is open - **Focus Restoration**: Focus returns to the trigger element when closed ### Keyboard Navigation | Key | Action | |-----|--------| | `Escape` | Closes the dialog (unless `static_backdrop: true`) | | `Tab` | Cycles through focusable elements within the dialog | ## Dark Mode The dialog fully supports dark mode with appropriate color adjustments: - Background: `bg-white dark:bg-zinc-900` - Border colors: `border-zinc-200 dark:border-zinc-700` - Text colors: Automatically adapt for readability - Backdrop: Works with all backdrop variants Colors automatically adapt when the `dark` class is present on ``. ## Animations The dialog uses CSS-only animations with `@starting-style` for smooth entry/exit: - **Center (Modal)**: Scale from 95% to 100% with fade - **Right/Left (Drawer)**: Slide in from edge with fade - **Top/Bottom (Sheet)**: Slide in from edge with fade - **Backdrop**: Smooth fade in/out Animation classes are defined in the safelist.css and require no JavaScript. --- ## Dropdown # Dropdown Component Usage The Dropdown component provides a versatile menu system for navigation, selection, or actions. ## Features - **Two Modes**: Action dropdowns (navigation/menu) and Selection dropdowns (form integration) - **Multiple Variants**: solid, outline, ghost, soft - **Semantic Colors**: zinc, primary, success, warning, danger, info - **Various Sizes**: xs, sm, base, lg, xl - **Multiple Shapes**: square, rounded, pill - **Collection Support**: Hash, Array, ActiveRecord::Relation - **Keyboard Navigation**: Arrow keys, Escape, Enter - **Accessibility**: Full ARIA support - **Slots**: items, headers, dividers, icons - **Form Builder Integration**: f.rui_dropdown - **Placement Options**: top, right, bottom, left - **Trigger Options**: click, hover - **Full Dark Mode Support** ## Basic Usage ### Action Dropdown (Navigation/Menu) ```erb <%# Simple action dropdown %> <%= rui_dropdown(text: "Options") do |dropdown| %> <% dropdown.with_item(text: "Edit", href: edit_path) %> <% dropdown.with_item(text: "Delete", href: delete_path) %> <% end %> <%# With header and divider %> <%= rui_dropdown(text: "Menu", color: :primary) do |dropdown| %> <% dropdown.with_header(text: "Actions") %> <% dropdown.with_item(text: "Edit", href: edit_path, icon: :edit) %> <% dropdown.with_item(text: "Duplicate", href: duplicate_path, icon: :copy) %> <% dropdown.with_divider %> <% dropdown.with_header(text: "Dangerous") %> <% dropdown.with_item(text: "Delete", href: delete_path, icon: :trash_2) %> <% end %> ``` ### Selection Dropdown (Form Integration) ```erb <%# Standalone with collection %> <%= rui_dropdown(:category, collection: Category.all, label: "Category") %> <%# With Hash collection %> <%= rui_dropdown( :status, collection: { "Draft" => "draft", "Published" => "published" }, label: "Status", color: :primary ) %> <%# Form builder %> <%= form_with(model: @post) do |f| %> <%= f.rui_dropdown( :category_id, collection: Category.all, label: "Category", help_text: "Choose a category for your post" ) %> <%= f.rui_dropdown( :status, collection: Post.statuses, label: "Status", required: true ) %> <% end %> ``` ## Parameters ### Required (for selection mode) - `method` - Symbol, the model attribute name (when using with form or collection) - `collection` - Array/Hash/ActiveRecord::Relation, the collection of options (for selection mode) ### Required (for action mode) - `text` - String, button text for action dropdowns ### Optional Styling - `variant` - Symbol, visual style (:solid, :outline, :ghost, :soft) - default: :solid - `color` - Symbol, color theme (:zinc, :primary, :success, :warning, :danger, :info) - default: :zinc - `size` - Symbol, button size (:xs, :sm, :base, :lg, :xl) - default: :base - `shape` - Symbol, button shape (:square, :rounded, :pill) - default: :rounded - `width` - Symbol, menu width (:sm, :base, :lg, :xl, :full) - default: :base ### Optional Configuration - `placement` - Symbol, menu position (:top, :right, :bottom, :left) - default: :bottom - `trigger` - Symbol, how to open (:click, :hover) - default: :click - `disabled` - Boolean, disable the dropdown - default: false - `required` - Boolean, show required indicator - default: false ### Form Integration - `label` - String, label text - `help_text` - String, help text below dropdown - `prompt_text` - String, text when no option selected - default: "Select an option" - `include_prompt` - Boolean, include prompt as selectable option - default: true ### Collection Processing - `value_method` - Symbol, method to get value from items - default: :id - `text_method` - Symbol, method to get label from items - default: :name ### HTML Attributes - Any additional HTML attributes are passed to the wrapper div ### Item Parameters (for `with_item`) When adding items to the dropdown using `dropdown.with_item()`, these parameters are available: - `text` - String, the item text (required) - `href` - String, link URL (optional, renders as link if present, button otherwise) - `value` - String, value for selection mode (optional) - `icon` - Symbol, icon name to display (optional) - `disabled` - Boolean, disable the item - default: false - `danger` - Boolean, apply destructive/warning styling - default: false - `active` - Boolean, mark item as current/active page - default: false - `active_class` - String, custom classes to apply ONLY when active is true (optional) - `class` - String, custom classes to apply to ALL states (optional) - `data` - Hash, custom data attributes (optional) **Key Differences:** - `active_class:` → Only applies when `active: true` (recommended for current page styling) - `class:` → Always applies regardless of active state (use for consistent styling) ## Examples ### With All Variants ```erb <%= rui_dropdown(text: "Solid", variant: :solid, color: :primary) do |d| %> <% d.with_item(text: "Action", href: "#") %> <% end %> <%= rui_dropdown(text: "Outline", variant: :outline, color: :primary) do |d| %> <% d.with_item(text: "Action", href: "#") %> <% end %> <%= rui_dropdown(text: "Ghost", variant: :ghost, color: :primary) do |d| %> <% d.with_item(text: "Action", href: "#") %> <% end %> <%= rui_dropdown(text: "Soft", variant: :soft, color: :primary) do |d| %> <% d.with_item(text: "Action", href: "#") %> <% end %> ``` ### With All Sizes ```erb <%= rui_dropdown(text: "XS", size: :xs) { ... } %> <%= rui_dropdown(text: "SM", size: :sm) { ... } %> <%= rui_dropdown(text: "Base", size: :base) { ... } %> <%= rui_dropdown(text: "LG", size: :lg) { ... } %> <%= rui_dropdown(text: "XL", size: :xl) { ... } %> ``` ### With All Shapes ```erb <%= rui_dropdown(text: "Square", shape: :square) { ... } %> <%= rui_dropdown(text: "Rounded", shape: :rounded) { ... } %> <%= rui_dropdown(text: "Pill", shape: :pill) { ... } %> ``` ### With All Placements ```erb <%= rui_dropdown(text: "Top", placement: :top) { ... } %> <%= rui_dropdown(text: "Right", placement: :right) { ... } %> <%= rui_dropdown(text: "Bottom", placement: :bottom) { ... } %> <%= rui_dropdown(text: "Left", placement: :left) { ... } %> ``` ### With Icon ```erb <%= rui_dropdown(text: "Actions", color: :primary) do |dropdown| %> <% dropdown.with_icon(:menu) %> <% dropdown.with_item(text: "Edit", href: edit_path, icon: :edit) %> <% dropdown.with_item(text: "Delete", href: delete_path, icon: :trash_2) %> <% end %> ``` ### With Hover Trigger ```erb <%= rui_dropdown(text: "Hover Me", trigger: :hover) do |dropdown| %> <% dropdown.with_item(text: "Action 1", href: "#") %> <% dropdown.with_item(text: "Action 2", href: "#") %> <% end %> ``` ### With Disabled Items ```erb <%= rui_dropdown(text: "Options") do |dropdown| %> <% dropdown.with_item(text: "Enabled", href: "#") %> <% dropdown.with_item(text: "Disabled", href: "#", disabled: true) %> <% dropdown.with_item(text: "Enabled", href: "#") %> <% end %> ``` ### With ActiveRecord Collection ```erb <%# Basic ActiveRecord collection %> <%= rui_dropdown(:author_id, collection: User.all, label: "Author") %> <%# Custom methods %> <%= rui_dropdown( :author_id, collection: User.all, value_method: :id, text_method: :full_name, label: "Author" ) %> ``` ### With Array of Pairs ```erb <%= rui_dropdown( :language, collection: [["Ruby", "rb"], ["JavaScript", "js"], ["Python", "py"]], label: "Language" ) %> ``` ### With Custom HTML Attributes ```erb <%= rui_dropdown( text: "Options", id: "custom-dropdown", class: "my-custom-class", data: { custom: "value" } ) do |dropdown| %> <% dropdown.with_item(text: "Action", href: "#") %> <% end %> ``` ## Slots ### Items Slot Add individual menu items: ```erb <%= rui_dropdown(text: "Options") do |dropdown| %> <% dropdown.with_item( text: "Edit", href: edit_path, icon: :edit, disabled: false ) %> <% end %> ``` ### Headers Slot Add section headers: ```erb <%= rui_dropdown(text: "Menu") do |dropdown| %> <% dropdown.with_header(text: "Section 1") %> <% dropdown.with_item(text: "Action 1", href: "#") %> <% dropdown.with_header(text: "Section 2") %> <% dropdown.with_item(text: "Action 2", href: "#") %> <% end %> ``` ### Dividers Slot Add visual separators: ```erb <%= rui_dropdown(text: "Options") do |dropdown| %> <% dropdown.with_item(text: "Edit", href: "#") %> <% dropdown.with_divider %> <% dropdown.with_item(text: "Delete", href: "#") %> <% end %> ``` ### Icon Slot Add an icon to the trigger button: ```erb <%= rui_dropdown(text: "Actions") do |dropdown| %> <% dropdown.with_icon(:menu) %> <% dropdown.with_item(text: "Action", href: "#") %> <% end %> ``` ### Active/Current State Mark navigation items as active/current to highlight the current page: ```erb <%= rui_dropdown(text: "Navigation") do |dropdown| %> <% dropdown.with_item( text: "Dashboard", href: dashboard_path, icon: :home, active: request.path == dashboard_path ) %> <% dropdown.with_item( text: "Settings", href: settings_path, icon: :settings, active: request.path.start_with?(settings_path) ) %> <% dropdown.with_item( text: "Reports", href: reports_path, icon: :chart_bar, active: current_page?(reports_path) ) %> <% end %> ``` **Features:** - Active items styled with primary color and medium font weight (default) - Use `active_class:` to customize active styling (only applies when active) - Developer controls matching logic (exact path, prefix, etc.) - Active state takes precedence over danger state **Custom Active Styling with `active_class:`** The `active_class:` parameter ONLY applies when `active: true`. This is the recommended way to customize active item styling: ```erb <%# Custom active styling - only applies to current page %> <%= rui_dropdown(text: "Navigation") do |dropdown| %> <% dropdown.with_item( text: "Dashboard", href: dashboard_path, active: current_page?(dashboard_path), active_class: "bg-blue-50 text-blue-900 font-bold border-l-4 border-blue-600" ) %> <% dropdown.with_item( text: "Reports", href: reports_path, active: current_page?(reports_path), active_class: "bg-emerald-50 text-emerald-900 font-semibold" ) %> <% end %> ``` **Using `class:` parameter (applies to ALL states)** The `class:` parameter applies to both active and inactive states: ```erb <%# This border applies to BOTH active and inactive states %> <% dropdown.with_item( text: "Dashboard", href: dashboard_path, active: current_page?(dashboard_path), class: "border-l-2" # Always has left border ) %> ``` **Combining both parameters:** ```erb <%# Border always present, custom colors only when active %> <% dropdown.with_item( text: "Dashboard", href: dashboard_path, active: current_page?(dashboard_path), active_class: "bg-blue-50 text-blue-900", class: "border-l-2" ) %> ``` ## Keyboard Navigation - **Arrow Down** - Focus next item - **Arrow Up** - Focus previous item - **Escape** - Close menu and return focus to trigger - **Enter** - Select focused item (or open/close dropdown) - **Tab** - Close menu and move to next focusable element ## Accessibility The component includes full ARIA support: - `role="menu"` on dropdown menu - `role="menuitem"` on dropdown items - `aria-expanded` on trigger button - `aria-haspopup="true"` on trigger button - `aria-controls` linking trigger to menu - `aria-labelledby` linking menu to trigger - `aria-required` when required - `aria-invalid` when errors present - `aria-describedby` for help text and errors ## Form Integration Notes ### Selection Mode vs Action Mode The component automatically detects the mode: - **Selection Mode**: When `collection` AND (`form` OR `method`) are present - Creates hidden input for form submission - Updates button text on selection - Includes prompt option - **Action Mode**: Otherwise - No hidden input - Button text stays unchanged - No prompt option ### With Form Builder ```erb <%= form_with(model: @post) do |f| %> <%= f.rui_dropdown(:status, collection: Post.statuses) %> <% end %> ``` This generates: - Correct name attribute: `post[status]` - Correct ID: `post_status` - Auto-retrieves value from `@post.status` - Shows errors from `@post.errors[:status]` ## Customization The component uses Tailwind CSS classes defined in `dropdown/styles.rb`. All styling is controlled via Ruby constants and can be customized by modifying the styles module. ## Dark Mode All variants automatically include dark mode support via Tailwind's `dark:` variants. --- ## Editable # Editable Component Inline editing component - click to edit text, blur or Enter to save, Escape to cancel. ## Basic Usage ```erb <%# Basic inline editing %> <%= rui_editable @post, :title, url: post_path(@post) %> <%# With placeholder %> <%= rui_editable @post, :subtitle, url: post_path(@post), placeholder: "Add a subtitle..." %> ``` ## Input Types ```erb <%# Text (default) %> <%= rui_editable @post, :title, url: post_path(@post), input_type: :text %> <%# Textarea %> <%= rui_editable @post, :body, url: post_path(@post), input_type: :textarea, rows: 4 %> <%# Number %> <%= rui_editable @product, :price, url: product_path(@product), input_type: :number, min: 0, max: 10000 %> ``` ## Custom Tags ```erb <%# As heading %> <%= rui_editable @post, :title, url: post_path(@post), tag: :h1, class: "text-3xl font-bold" %> <%# As paragraph %> <%= rui_editable @post, :excerpt, url: post_path(@post), tag: :p %> ``` ## Validation ```erb <%# Required field %> <%= rui_editable @user, :name, url: user_path(@user), required: true %> <%# Character limits %> <%= rui_editable @post, :title, url: post_path(@post), minlength: 3, maxlength: 100 %> <%# Number constraints %> <%= rui_editable @product, :quantity, url: product_path(@product), input_type: :number, min: 0, max: 999 %> ``` ## Controller Setup ```ruby class PostsController < ApplicationController def update @post = Post.find(params[:id]) if @post.update(post_params) respond_to do |format| format.turbo_stream { head :ok } format.html { redirect_to @post } end else respond_to do |format| format.turbo_stream { render turbo_stream: turbo_stream.replace( "editable_post_#{@post.id}_title", partial: "posts/title_error", locals: { post: @post } ) } format.json { render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity } end end end private def post_params params.require(:post).permit(:title, :body) end end ``` ## Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model` | ActiveRecord::Base | required | The model object to edit | | `attribute` | Symbol | required | The attribute name to edit | | `url` | String | nil | PATCH endpoint URL | | `tag` | Symbol | :span | Wrapper element | | `placeholder` | String | "Click to edit" | Text when empty | | `input_type` | Symbol | :text | Input type (:text, :textarea, :number) | | `required` | Boolean | false | Whether field is required | | `maxlength` | Integer | nil | Maximum character length | | `minlength` | Integer | nil | Minimum character length | | `min` | Integer | nil | Minimum value (number type) | | `max` | Integer | nil | Maximum value (number type) | | `rows` | Integer | 3 | Rows (textarea type) | | `id` | String | auto | Custom ID | | `class` | String | nil | Custom CSS classes | ## Keyboard Shortcuts - **Enter** - Save changes (text/number input) - **Cmd/Ctrl+Enter** - Save changes (textarea) - **Escape** - Cancel edits, revert to original value - **Tab** - Move to next element (triggers save) ## Stimulus Controller The component uses the `editable` Stimulus controller with: ### Values - `url` - PATCH endpoint - `field` - Form field name - `current` - Current value - `inputType` - Input type - `placeholder` - Placeholder text ### Targets - `display` - Text display element - `input` - Input/textarea element - `saving` - Saving indicator - `error` - Error message ### Actions - `edit` - Switch to edit mode - `save` - Save to server - `cancel` - Cancel edits --- ## Icon # Icon Component Use a beautiful set of 1500+ open-source SVG icons powered by the excellent Lucide icon library. ## Requirements The Icon component requires the `lucide-rails` gem to be installed: ```ruby # Gemfile gem 'lucide-rails' ``` ```bash bundle install ``` Browse available icons at: **https://lucide.dev/icons** --- ## Basic Usage ### Standalone Icon ```erb <%= rui_icon :heart %> <%= rui_icon :check, color: :success %> <%= rui_icon :x_circle, color: :danger, size: :lg %> ``` ### Common Icons ```erb <%= rui_icon :menu %> <%= rui_icon :x %> <%= rui_icon :search %> <%= rui_icon :settings %> <%= rui_icon :check, color: :success %> <%= rui_icon :alert_circle, color: :warning %> <%= rui_icon :x_circle, color: :danger %> <%= rui_icon :info, color: :info %> <%= rui_icon :arrow_right %> <%= rui_icon :chevron_down %> <%= rui_icon :external_link %> ``` --- ## Sizes Icon sizes range from `xs` (12px) to `xl6` (80px): ```erb <%= rui_icon :heart, size: :xs %> <%= rui_icon :heart, size: :sm %> <%= rui_icon :heart, size: :base %> <%= rui_icon :heart, size: :lg %> <%= rui_icon :heart, size: :xl %> <%= rui_icon :heart, size: :xl2 %> <%= rui_icon :heart, size: :xl3 %> <%= rui_icon :heart, size: :xl4 %> <%= rui_icon :heart, size: :xl5 %> <%= rui_icon :heart, size: :xl6 %> ``` --- ## Colors ### Semantic Colors ```erb <%= rui_icon :check, color: :primary %> <%= rui_icon :check, color: :secondary %> <%= rui_icon :check, color: :success %> <%= rui_icon :alert_triangle, color: :warning %> <%= rui_icon :x_circle, color: :danger %> <%= rui_icon :info, color: :info %> ``` ### Tailwind Colors ```erb <%= rui_icon :heart, color: :red %> <%= rui_icon :star, color: :yellow %> <%= rui_icon :circle, color: :blue %> <%= rui_icon :square, color: :'purple-500' %> ``` ### Inherit Parent Color Use `standalone: false` to inherit color from the parent as: ```erb
<%= rui_icon :check, standalone: false %> This text and icon share the same color
``` --- ## Usage with Buttons Icons work great inside buttons: ```erb <%= rui_button color: :primary do %> <%= rui_icon :save, size: :sm, standalone: false %> Save <% end %> <%= rui_button color: :success do %> <%= rui_icon :check, size: :sm, standalone: false, class: "mr-2" %> Confirm <% end %> <%= rui_button color: :danger do %> Delete <%= rui_icon :trash_2, size: :sm, standalone: false, class: "ml-2" %> <% end %> <%= rui_button color: :primary, class: "px-3" do %> <%= rui_icon :settings, size: :base, standalone: false %> <% end %> ``` --- ## Accessibility ### Decorative Icons Icons that are purely decorative (next to text) should be hidden from screen readers: ```erb <%= rui_button do %> <%= rui_icon :save, size: :sm, standalone: false, aria_hidden: true %> Save Post <% end %> ``` ### Semantic Icons Icons conveying meaning should have labels: ```erb <%= rui_icon :x, aria_label: "Close modal", class: "cursor-pointer" %> <%= rui_icon :heart %> ``` ### Icon-Only Buttons When buttons contain only an icon, provide an accessible label: ```erb <%= rui_button color: :primary, aria_label: "Close" do %> <%= rui_icon :x, standalone: false, aria_hidden: true %> <% end %> ``` --- ## Common Patterns ### Status Indicators ```erb <% if @post.published? %> <%= rui_icon :check_circle, color: :success, size: :sm %> Published <% else %> <%= rui_icon :circle, color: :secondary, size: :sm %> Draft <% end %> ``` ### Loading State ```erb <% if @loading %> <%= rui_icon :loader_2, class: "animate-spin" %> Loading... <% else %> <%= rui_icon :check, color: :success %> Complete <% end %> ``` ### Navigation ```erb <%= link_to posts_path, class: "flex items-center gap-2" do %> <%= rui_icon :arrow_left, size: :sm, standalone: false %> Back to Posts <% end %> <%= link_to post_path(@post), class: "flex items-center gap-2" do %> View Post <%= rui_icon :external_link, size: :sm, standalone: false %> <% end %> ``` ### Form Fields ```erb
<%= form.text_field :email, class: "pl-10" %>
<%= rui_icon :mail, size: :sm %>
``` ### Alerts ```erb
<%= rui_icon :check_circle, color: :success %>

Success!

Your post has been published.

``` --- ## Dark Mode All icon colors support dark mode automatically: ```erb <%= rui_icon :sun, color: :primary %> ``` Use `standalone: false` for icons that should match surrounding text: ```erb

<%= rui_icon :info, size: :sm, standalone: false %> This icon matches the text color in both modes

``` --- ## Icon Name Formats Icon names are automatically converted from snake_case to kebab-case: ```erb <%= rui_icon :alert_circle %> <%= rui_icon :x_circle %> <%= rui_icon :chevron_down %> ``` Both formats work: ```erb <%= rui_icon :arrow_right %> <%= rui_icon "arrow-right" %> ``` --- ## Custom Attributes Add any HTML attributes: ```erb <%= rui_icon :heart, class: "hover:scale-110 transition-transform cursor-pointer", data: { action: "click->heart#toggle" }, id: "favorite-icon" %> ``` --- ## Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `name` | Symbol/String | **required** | Lucide icon name | | `size` | Symbol | `:base` | Icon size (`:xs` to `:xl6`) | | `color` | Symbol | `:inherit` | Icon color (semantic or Tailwind) | | `standalone` | Boolean | `true` | If false, inherits parent color | | `aria_label` | String | `nil` | Accessible label for icon | | `aria_hidden` | Boolean | `false` | Hide from screen readers | | `**options` | Hash | `{}` | Additional HTML attributes | --- ## Examples in Posts ### Index Page ```erb

<%= rui_icon :file_text, size: :lg, color: :primary %> All Posts

<%= link_to new_post_path, class: "inline-flex items-center gap-2" do %> <%= rui_icon :plus, size: :sm, standalone: false %> New Post <% end %> <% @posts.each do |post| %>

<% if post.published? %> <%= rui_icon :check_circle, color: :success, size: :sm %> <% else %> <%= rui_icon :circle, color: :secondary, size: :sm %> <% end %> <%= post.title %>

<%= link_to edit_post_path(post) do %> <%= rui_icon :edit, size: :sm, class: "text-blue-600" %> <% end %> <%= rui_button_to post_path(post), method: :delete, data: { turbo_confirm: "Delete this post?" } do %> <%= rui_icon :trash_2, size: :sm, standalone: false %> <% end %>
<% end %> ``` ### Show Page ```erb
<%= link_to posts_path, class: "inline-flex items-center gap-2 text-blue-600" do %> <%= rui_icon :arrow_left, size: :sm, standalone: false %> Back <% end %>
<%= rui_button color: :primary, href: edit_post_path(@post) do %> <%= rui_icon :edit, size: :sm, standalone: false, class: "mr-2" %> Edit <% end %> <%= rui_button_to post_path(@post), method: :delete, color: :danger do %> <%= rui_icon :trash_2, size: :sm, standalone: false, class: "mr-2" %> Delete <% end %> end ``` --- ## Tips 1. **Use `standalone: false`** when icons are inside colored containers or buttons 2. **Set `aria_hidden: true`** for decorative icons next to text 3. **Use semantic sizes** - `:sm` for inline text, `:base` for buttons, `:lg` for headings 4. **Browse icons at** https://lucide.dev/icons to find the perfect icon 5. **Icon names use kebab-case** - `arrow-right`, `check-circle`, `x-mark` --- ## See Also - Button Component - Combine icons with buttons - Lucide Icon Library - https://lucide.dev/icons - lucide-rails gem - https://github.com/heyvito/lucide-rails --- ## Image # Image Component A semantic HTML image component for displaying content images with captions, links, and visual effects. ## Features - **Semantic HTML**: Uses `
` and `
` when caption is present - **Picture Element**: Art direction and format fallback with `` and `` elements - **Performance**: Built-in lazy loading, async decoding, and fetchpriority - **Responsive**: Support for srcset and sizes attributes - **Visual Effects**: Grayscale, blur, sepia, zoom, shine, and overlay effects - **Flexible Layout**: Multiple sizes, shapes, aspect ratios, and alignments - **Linkable**: Wrap images in links with proper styling - **Social Media Sizes**: Built-in presets for Instagram, Facebook, X, LinkedIn, Pinterest, TikTok, and YouTube ## Basic Usage ```erb <%# Simple image (positional src argument) %> <%= rui_image("photo.jpg", alt: "A beautiful sunset") %> <%# Alternative: keyword argument for src %> <%= rui_image(src: "photo.jpg", alt: "A beautiful sunset") %> <%# Image with caption (uses figure/figcaption) %> <%= rui_image( "landscape.jpg", alt: "Mountain vista", caption: "View from the summit at sunrise" ) %> <%# Linked image %> <%= rui_image( "product.jpg", alt: "Product name", url: product_path(@product) ) %> ``` ## Parameters ### Required | Parameter | Type | Description | |-----------|------|-------------| | `src` | String | Image source URL (can be passed as first positional argument) | | `alt` | String | Alt text for accessibility | ### Optional - Content | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `caption` | String | nil | Image caption (triggers figure/figcaption) | | `url` | String | nil | Link URL (makes image clickable) | | `sources` | Array | nil | Picture element sources for art direction/format fallback (see Picture Element section) | ### Optional - Size & Shape | Parameter | Type | Default | Options | |-----------|------|---------|---------| | `size` | Symbol | `:auto` | `:auto`, `:full`, `:xs`, `:sm`, `:md`, `:lg`, `:xl`, `:xl2`, `:xl3`, or platform presets (see below) | | `shape` | Symbol | `:default` | `:default`, `:rounded`, `:circle`, `:square` | | `ratio` | Symbol | `:auto` | `:auto`, `:square`, `:video`, `:portrait`, `:landscape`, `:wide`, `:ultrawide` | | `fit` | Symbol | `:cover` | `:cover`, `:contain`, `:fill`, `:none`, `:scale_down` | | `position` | Symbol | `:center` | `:center`, `:top`, `:bottom`, `:left`, `:right`, `:left_top`, `:left_bottom`, `:right_top`, `:right_bottom` | | `alignment` | Symbol | `:left` | `:left`, `:center`, `:right` (figure alignment) | **Note:** When using platform preset sizes, the aspect ratio is automatically applied. The `ratio` parameter is ignored for platform sizes. ### Optional - Performance | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `loading` | Symbol | `:lazy` | `:lazy` or `:eager` | | `decoding` | Symbol | `:async` | `:async`, `:sync`, or `:auto` | | `fetchpriority` | Symbol | `:auto` | `:high`, `:low`, or `:auto` | | `srcset` | String | nil | Responsive image sources | | `sizes` | String | nil | Responsive image sizes | | `width` | Integer | nil | Image width (prevents layout shift) | | `height` | Integer | nil | Image height (prevents layout shift) | ### Optional - Security | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `crossorigin` | String | nil | CORS setting (`anonymous`, `use-credentials`) | | `referrerpolicy` | String | nil | Referrer policy for the request | ### Optional - Visual Effects | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `grayscale` | Boolean | false | Apply grayscale effect (restores on hover) | | `blur` | Boolean | false | Apply blur effect (restores on hover) | | `sepia` | Boolean | false | Apply sepia effect (restores on hover) | | `zoom` | Boolean | false | Apply zoom effect on hover | | `shine` | Boolean | false | Apply shine effect on hover | | `overlay` | Boolean | false | Apply dark overlay on hover | ## Size Reference | Size | Dimensions | |------|------------| | `:auto` | Natural size with max-w-full | | `:full` | 100% width | | `:xs` | 64px (w-16 h-16) | | `:sm` | 96px (w-24 h-24) | | `:md` | 128px (w-32 h-32) | | `:lg` | 192px (w-48 h-48) | | `:xl` | 256px (w-64 h-64) | | `:xl2` | 320px (w-80 h-80) | | `:xl3` | 384px (w-96 h-96) | ## Aspect Ratio Reference | Ratio | Value | |-------|-------| | `:auto` | Natural aspect ratio | | `:square` | 1:1 | | `:video` | 16:9 | | `:portrait` | 3:4 | | `:landscape` | 4:3 | | `:wide` | 16:9 | | `:ultrawide` | 21:9 | ## Social Media Platform Sizes Built-in presets for common social media image dimensions (based on 2025 guidelines). These presets include the correct aspect ratio automatically. ### Instagram | Size | Aspect Ratio | Original Dimensions | Description | |------|--------------|---------------------|-------------| | `:ig_post` | 1:1 | 1080×1080 | Standard feed post | | `:ig_post_square` | 1:1 | 1080×1080 | Square feed post | | `:ig_post_portrait` | 4:5 | 1080×1350 | Portrait feed post | | `:ig_post_landscape` | 1.91:1 | 1080×566 | Landscape feed post | | `:ig_carousel` | 4:5 | 1080×1350 | Carousel post | | `:ig_story` | 9:16 | 1080×1920 | Story | | `:ig_reel` | 9:16 | 1080×1920 | Reel | ### Facebook | Size | Aspect Ratio | Original Dimensions | Description | |------|--------------|---------------------|-------------| | `:fb_post` | 1:1 | 1080×1080 | Standard post | | `:fb_post_square` | 1:1 | 1080×1080 | Square post | | `:fb_post_portrait` | 4:5 | 1080×1350 | Portrait post | | `:fb_post_landscape` | 1.91:1 | 1080×566 | Landscape post | | `:fb_story` | 9:16 | 1080×1920 | Story | | `:fb_cover` | 2.7:1 | 851×315 | Cover photo | ### X (Twitter) | Size | Aspect Ratio | Original Dimensions | Description | |------|--------------|---------------------|-------------| | `:x_post` | 16:9 | 1280×720 | Standard post | | `:x_post_square` | 1:1 | 720×720 | Square post | | `:x_post_portrait` | 9:16 | 720×1280 | Portrait post | | `:x_header` | 3:1 | 1500×500 | Header/banner | | `:x_banner` | 3:1 | 1500×500 | Banner (alias) | ### LinkedIn | Size | Aspect Ratio | Original Dimensions | Description | |------|--------------|---------------------|-------------| | `:linkedin_post` | 1.91:1 | 1200×627 | Standard post | | `:linkedin_post_square` | 1:1 | 1200×1200 | Square post | | `:linkedin_post_portrait` | 4:5 | 720×900 | Portrait post | | `:linkedin_cover` | 5.9:1 | 1128×191 | Cover image | | `:linkedin_banner` | 5.9:1 | 1128×191 | Banner (alias) | ### Pinterest | Size | Aspect Ratio | Original Dimensions | Description | |------|--------------|---------------------|-------------| | `:pin` | 2:3 | 1000×1500 | Standard pin | | `:pin_square` | 1:1 | 1000×1000 | Square pin | ### TikTok | Size | Aspect Ratio | Original Dimensions | Description | |------|--------------|---------------------|-------------| | `:tiktok_post` | 9:16 | 1080×1920 | Post/video | | `:tiktok_video` | 9:16 | 1080×1920 | Video (alias) | ### YouTube | Size | Aspect Ratio | Original Dimensions | Description | |------|--------------|---------------------|-------------| | `:yt_thumbnail` | 16:9 | 1280×720 | Video thumbnail | | `:yt_banner` | 16:9 | 2560×1440 | Channel banner | ### Platform Size Examples ```erb <%# Instagram feed post %> <%= rui_image("social/campaign.jpg", alt: "Summer campaign", size: :ig_post) %> <%# Instagram story %> <%= rui_image("social/story.jpg", alt: "New product", size: :ig_story) %> <%# Facebook cover %> <%= rui_image("social/cover.jpg", alt: "Company cover", size: :fb_cover) %> <%# X (Twitter) post %> <%= rui_image("social/announcement.jpg", alt: "Big news", size: :x_post) %> <%# LinkedIn article image %> <%= rui_image("social/article.jpg", alt: "Industry insights", size: :linkedin_post) %> <%# Pinterest pin %> <%= rui_image("social/pin.jpg", alt: "Recipe idea", size: :pin) %> <%# YouTube thumbnail %> <%= rui_image("social/thumb.jpg", alt: "Video title", size: :yt_thumbnail) %> ``` ## Examples ### Gallery Image with Caption ```erb <%= rui_image( "gallery/sunset.jpg", alt: "Sunset over the Pacific Ocean", caption: "Malibu, California - Summer 2024", shape: :rounded, alignment: :center ) %> ``` ### Product Image with Link ```erb <%= rui_image( @product.image_url, alt: @product.name, url: product_path(@product), size: :lg, shape: :rounded, zoom: true ) %> ``` ### Hero Image (Above the Fold) ```erb <%= rui_image( "hero-banner.jpg", alt: "Welcome to our store", size: :full, ratio: :wide, loading: :eager, fetchpriority: :high, decoding: :sync ) %> ``` ### Responsive Image ```erb <%= rui_image( "photo.jpg", alt: "Responsive photo", srcset: "photo-320.jpg 320w, photo-640.jpg 640w, photo-1280.jpg 1280w", sizes: "(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw", width: 1280, height: 960 ) %> ``` ### Team Member Photo ```erb <%= rui_image( team_member.photo_url, alt: "#{team_member.name}, #{team_member.role}", caption: "#{team_member.name} - #{team_member.role}", size: :md, shape: :circle, grayscale: true ) %> ``` ### Image with Visual Effects ```erb <%# Grayscale that restores color on hover %> <%= rui_image( "portfolio/project1.jpg", alt: "Project screenshot", grayscale: true, zoom: true ) %> <%# Blurred image with hover reveal %> <%= rui_image("preview.jpg", alt: "Preview", blur: true) %> <%# Multiple effects %> <%= rui_image( "featured.jpg", alt: "Featured image", sepia: true, zoom: true, shine: true ) %> ``` **Technical Note**: The `shine` and `overlay` effects use CSS pseudo-elements (`::before`/`::after`) which don't work on `` elements directly. When these effects are enabled, the component automatically wraps the image in a `` element to properly render the effects. ### Cross-Origin Image ```erb <%= rui_image( "https://cdn.example.com/image.jpg", alt: "CDN hosted image", crossorigin: "anonymous", referrerpolicy: "no-referrer" ) %> ``` ## Picture Element The `` element provides advanced image handling for two main use cases: 1. **Art Direction**: Show different images for different viewports (e.g., cropped mobile vs wide desktop) 2. **Format Fallback**: Serve modern formats (AVIF, WebP) with fallback to JPEG/PNG ### When to Use Picture vs srcset | Use Case | Solution | |----------|----------| | Same image, different sizes | Use `srcset` and `sizes` on regular image | | Different images for different viewports | Use `` with `sources` | | Modern format with fallback | Use `` with `sources` | | Dark mode alternative image | Use `` with `sources` | ### Source Options Each source in the `sources` array accepts: | Key | Type | Required | Description | |-----|------|----------|-------------| | `srcset` | String | Yes | Image source(s) for this source element | | `media` | String | No | Media query (e.g., "(min-width: 1024px)") | | `type` | String | No | MIME type (e.g., "image/avif", "image/webp") | | `sizes` | String | No | Responsive sizes for this source | | `width` | Integer | No | Width hint for browser | | `height` | Integer | No | Height hint for browser | ### Art Direction Example Show different image crops based on viewport: ```erb <%# Different images for mobile vs desktop %> <%= rui_image( "hero-fallback.jpg", alt: "Product showcase", sources: [ { srcset: "hero-wide.jpg", media: "(min-width: 1024px)" }, { srcset: "hero-square.jpg", media: "(min-width: 640px)" }, { srcset: "hero-portrait.jpg", media: "(max-width: 639px)" } ] ) %> ``` **Real-world use case**: E-commerce hero banner that shows a wide landscape shot on desktop but a tightly cropped product focus on mobile. ### Format Fallback Example Serve modern formats with automatic fallback: ```erb <%# AVIF → WebP → JPEG fallback chain %> <%= rui_image( "photo.jpg", alt: "High quality photo", sources: [ { srcset: "photo.avif", type: "image/avif" }, { srcset: "photo.webp", type: "image/webp" } ] ) %> ``` **Real-world use case**: Serve AVIF (50-90% smaller) to supported browsers, WebP to others, and JPEG as final fallback. ### Dark Mode Example Show different images based on color scheme preference: ```erb <%# Different images for light/dark mode %> <%= rui_image( "logo-light.png", alt: "Company logo", sources: [ { srcset: "logo-dark.png", media: "(prefers-color-scheme: dark)" } ] ) %> ``` **Real-world use case**: Logo that inverts colors or uses different contrast for dark mode. ### Combined Art Direction + Format Fallback ```erb <%# Art direction with format optimization %> <%= rui_image( "product-fallback.jpg", alt: "Product image", sources: [ # Desktop: wide format with modern formats { srcset: "product-wide.avif", media: "(min-width: 1024px)", type: "image/avif" }, { srcset: "product-wide.webp", media: "(min-width: 1024px)", type: "image/webp" }, { srcset: "product-wide.jpg", media: "(min-width: 1024px)" }, # Mobile: square format with modern formats { srcset: "product-square.avif", media: "(max-width: 1023px)", type: "image/avif" }, { srcset: "product-square.webp", media: "(max-width: 1023px)", type: "image/webp" }, { srcset: "product-square.jpg", media: "(max-width: 1023px)" } ] ) %> ``` ### Picture Element with Caption ```erb <%= rui_image( "artwork-fallback.jpg", alt: "Digital artwork", caption: "Created by Artist Name, 2024", sources: [ { srcset: "artwork.avif", type: "image/avif" }, { srcset: "artwork.webp", type: "image/webp" } ], shape: :rounded, alignment: :center ) %> ``` ### Picture Element with Link ```erb <%= rui_image( product.image_url, alt: product.name, url: product_path(product), sources: [ { srcset: product.avif_url, type: "image/avif" }, { srcset: product.webp_url, type: "image/webp" } ], zoom: true ) %> ``` ## Semantic HTML Output ### Without Caption ```html Description ``` ### With Caption ```html
Description
Caption text
``` ### With Link ```html
Description ``` ### With Caption and Link ```html
Description
Caption text
``` ### With Picture Element (Format Fallback) ```html Description ``` ### With Picture Element (Art Direction) ```html Description ``` ### Picture Element with Caption ```html
Description
Caption text
``` ### With Shine/Overlay Effects (Wrapper Element) ```html Description ``` ## Accessibility - Always provide meaningful `alt` text describing the image content - Use `caption` for additional context (renders as `
`) - Images are lazy loaded by default to improve page performance - Use `loading: :eager` and `fetchpriority: :high` for above-the-fold images ## Performance Tips 1. **Above the fold**: Use `loading: :eager`, `fetchpriority: :high`, `decoding: :sync` 2. **Below the fold**: Use defaults (`loading: :lazy`, `decoding: :async`) 3. **Prevent layout shift**: Always provide `width` and `height` attributes 4. **Responsive images**: Use `srcset` and `sizes` for optimal image delivery ## Real-World Advanced Examples ### CSS-Based Dark Mode (Recommended for Theme Toggles) Works with JavaScript theme toggles (like Tailwind's `dark:` classes or `data-theme`): ```erb <%# Show different images based on site theme (not OS preference) %>
<%# Light mode image - hidden in dark mode %>
<%= rui_image( "hero-light.jpg", alt: "Sunny office workspace", size: :full, ratio: :video, shape: :rounded ) %>
<%# Dark mode image - shown only in dark mode %>
``` **Use this for:** Logos, hero banners, and any images that need to adapt to the site's theme toggle. ### Responsive srcset within Source (Art Direction + Responsive Sizes) Different images per viewport, each with multiple resolution options: ```erb <%# Desktop gets landscape at 2 resolutions, tablet at 1, mobile fallback %> <%= rui_image( "hero-mobile.jpg", alt: "Product showcase", sources: [ { srcset: "hero-desktop-1200.jpg 1200w, hero-desktop-1600.jpg 1600w", media: "(min-width: 1024px)", sizes: "(min-width: 1400px) 1400px, 100vw" }, { srcset: "hero-tablet-600.jpg 600w, hero-tablet-900.jpg 900w", media: "(min-width: 640px)", sizes: "100vw" } ], width: 1600, height: 900 ) %> ``` **Use this for:** Hero banners and product images that need both art direction and responsive optimization. ### High DPI / Retina Images Serve high-resolution images for Retina/high-DPI displays using density descriptors: ```erb <%# Serve 2x and 3x density for Retina displays %> <%= rui_image( "logo.png", alt: "Company logo", srcset: "logo.png 1x, logo@2x.png 2x, logo@3x.png 3x" ) %> <%# Retina with art direction - desktop vs mobile %> <%= rui_image( "product-mobile.jpg", alt: "Product image", sources: [ { srcset: "product-desktop.jpg 1x, product-desktop@2x.jpg 2x", media: "(min-width: 768px)" }, { srcset: "product-mobile.jpg 1x, product-mobile@2x.jpg 2x" } ] ) %> ``` **Use this for:** Logos and images on high-DPI/Retina displays (iPhones, modern laptops, etc.). ### Blog Post Image with Semantic Layout Full article layout with responsive image and proper semantic HTML: ```erb
<%= rui_text("How We Built Our Design System", as: :h1, size: :3xl, weight: :bold, class: "mb-4") %> <%= rui_image( "blog/design-system.jpg", alt: "Engineering team working on design system", caption: "Our team collaborating on the design system", size: :full, ratio: :video, shape: :rounded, alignment: :center, loading: :eager, fetchpriority: :high, sources: [ { srcset: "blog/design-system.avif", type: "image/avif" }, { srcset: "blog/design-system.webp", type: "image/webp" } ] ) %> <%= rui_text( "Building a comprehensive design system requires careful planning, collaboration, and a deep understanding of your product's needs...", as: :p, size: :lg, color: :muted, class: "mt-6 leading-relaxed" ) %>
``` **Use this for:** Blog posts with large hero images that need semantic HTML and performance optimization. ## Image vs Avatar Use **Image** for: - Content images (photos, illustrations, diagrams) - Images with captions - Gallery images - Product images - Hero banners Use **Avatar** for: - User profile pictures - Team member photos (without captions) - Status indicators (online/offline) - Grouped/stacked avatars - Fallback to initials or icon --- ## Input # Input Component Text-based form input with ViewComponent + Tailwind CSS styling. ## Overview The Input component handles text-based input types only: - `text` - Standard text input (default) - `email` - Email address with browser validation - `password` - Obscured text with optional show/hide toggle - `number` - Numeric input with min/max/step support - `tel` - Phone number (displays phone keypad on mobile) - `url` - URL with browser validation - `search` - Search input with native clear button For file uploads, use the `Upload` component (future). For multi-line text, use the `Textarea` component (future). ## Basic Usage ### Standalone ```erb <%= rui_input(name: :search, type: :search, placeholder: "Search...") %> ``` ### In Form ```erb <%= form_with(model: @user) do |f| %> <%= f.rui_input(:email, type: :email, label: "Email Address") %> <%= f.rui_input(:password, type: :password, label: "Password") %> <%= f.rui_button %> <% end %> ``` ## Variants ```erb <%# Default - white background with border %> <%= rui_input(name: :name, label: "Name", variant: :default) %> <%# Filled - gray background %> <%= rui_input(name: :name, label: "Name", variant: :filled) %> <%# Outline - transparent background %> <%= rui_input(name: :name, label: "Name", variant: :outline) %> ``` ## Sizes ```erb <%= rui_input(name: :name, size: :xs) %> <%# Extra small %> <%= rui_input(name: :name, size: :sm) %> <%# Small %> <%= rui_input(name: :name, size: :base) %> <%# Default %> <%= rui_input(name: :name, size: :lg) %> <%# Large %> <%= rui_input(name: :name, size: :xl) %> <%# Extra large %> ``` ## Shapes ```erb <%= rui_input(name: :name, shape: :square) %> <%# No rounding %> <%= rui_input(name: :name, shape: :rounded) %> <%# Default rounded %> <%= rui_input(name: :name, shape: :pill) %> <%# Fully rounded %> ``` ## Input Types ### Text (default) ```erb <%= f.rui_input(:name, label: "Full Name", required: true) %> ``` ### Email ```erb <%= f.rui_input(:email, type: :email, label: "Email", autocomplete: "email") %> ``` ### Password with Show/Hide Toggle Password inputs automatically get a show/hide toggle button: ```erb <%= f.rui_input(:password, type: :password, label: "Password") %> ``` To disable the toggle, provide a custom suffix slot. ### Number with Range ```erb <%= f.rui_input(:quantity, type: :number, label: "Quantity", min: 1, max: 100, step: 1 ) %> <%# For decimals, use step: "any" %> <%= f.rui_input(:price, type: :number, label: "Price", min: 0, step: "any" ) %> ``` ### Telephone ```erb <%= f.rui_input(:phone, type: :tel, label: "Phone Number", placeholder: "(555) 123-4567", autocomplete: "tel" ) %> ``` ### URL ```erb <%= f.rui_input(:website, type: :url, label: "Website", placeholder: "https://example.com" ) %> ``` ### Search ```erb <%= rui_input( name: :query, type: :search, placeholder: "Search...", size: :lg ) %> ``` ## Prefix & Suffix Slots ### Text Prefix/Suffix ```erb <%= rui_input(name: :price, type: :number, label: "Price") do |input| %> <% input.with_prefix do %>$<% end %> <% input.with_suffix do %>.00<% end %> <% end %> ``` ### Icon Prefix ```erb <%= rui_input(name: :email, type: :email, label: "Email") do |input| %> <% input.with_prefix do %> <%= rui_icon(:mail, size: :sm, standalone: false) %> <% end %> <% end %> ``` ### Icon Suffix ```erb <%= rui_input(name: :search, type: :search, placeholder: "Search") do |input| %> <% input.with_suffix do %> <%= rui_icon(:search, size: :sm, standalone: false) %> <% end %> <% end %> ``` ## Validation Attributes ```erb <%= f.rui_input(:username, label: "Username", required: true, minlength: 3, maxlength: 20, pattern: "[a-zA-Z0-9_]+", help_text: "3-20 characters, letters, numbers, underscores only" ) %> ``` ## Help Text ```erb <%= f.rui_input(:email, type: :email, label: "Email", help_text: "We'll never share your email with anyone." ) %> ``` ## Error State Errors are automatically detected from Rails model errors: ```erb <%# If @user.errors[:email] has errors, the input shows error styling %> <%= f.rui_input(:email, type: :email, label: "Email") %> ``` ## Colors The color option controls the focus ring color: ```erb <%# Semantic colors %> <%= rui_input(name: :field, color: :primary) %> <%= rui_input(name: :field, color: :success) %> <%= rui_input(name: :field, color: :warning) %> <%= rui_input(name: :field, color: :danger) %> <%= rui_input(name: :field, color: :info) %> <%# Any Tailwind color %> <%= rui_input(name: :field, color: :indigo) %> <%= rui_input(name: :field, color: :pink) %> <%= rui_input(name: :field, color: :emerald) %> ``` ## States ### Disabled ```erb <%= f.rui_input(:name, label: "Name", disabled: true) %> ``` ### Readonly ```erb <%= f.rui_input(:api_key, label: "API Key", readonly: true) %> ``` ## Autocomplete ```erb <%# Common autocomplete values %> <%= f.rui_input(:name, autocomplete: "name") %> <%= f.rui_input(:email, type: :email, autocomplete: "email") %> <%= f.rui_input(:password, type: :password, autocomplete: "current-password") %> <%= f.rui_input(:new_password, type: :password, autocomplete: "new-password") %> <%= f.rui_input(:phone, type: :tel, autocomplete: "tel") %> <%# Disable autocomplete %> <%= f.rui_input(:otp, autocomplete: "off") %> ``` ## Input Mode Control virtual keyboard on mobile: ```erb <%# Numeric keyboard for PIN %> <%= f.rui_input(:pin, inputmode: "numeric", pattern: "[0-9]*") %> <%# Email keyboard %> <%= f.rui_input(:email, type: :email, inputmode: "email") %> <%# URL keyboard %> <%= f.rui_input(:website, type: :url, inputmode: "url") %> ``` ## Datalist (Autocomplete Suggestions) ```erb <%= f.rui_input(:browser, list: "browsers") %> ``` ## Hiding Validation Placeholder By default, inputs render a validation message placeholder that reserves space for error messages. When using inputs in filter bars or search forms where validation isn't needed, disable this to remove the gap: ```erb <%# In a filter bar - no validation placeholder %> <%= rui_input( name: :search, type: :search, placeholder: "Search...", validation: false ) %> ``` ## Custom Styling RapidRailsUI uses `tailwind_merge` to intelligently merge custom classes with defaults. Custom classes override conflicting default classes rather than being ignored. ### Input Element (class:) Override the main input element styling: ```erb <%# Custom border color %> <%= rui_input(name: :field, class: "border-purple-500 focus:ring-purple-500") %> <%# Custom background %> <%= rui_input(name: :field, class: "bg-yellow-50") %> <%# Multiple overrides %> <%= rui_input(name: :field, class: "border-2 border-indigo-500 rounded-none") %> ``` ### Wrapper (wrapper_class:) Override the outer container: ```erb <%# Fixed width %> <%= rui_input(name: :field, wrapper_class: "w-64") %> <%# Max width %> <%= rui_input(name: :field, wrapper_class: "max-w-xs") %> <%# Custom spacing %> <%= rui_input(name: :field, wrapper_class: "gap-3") %> ``` ### Label (label_class:) Override the label styling: ```erb <%# Custom label color %> <%= rui_input(name: :field, label: "Field", label_class: "text-indigo-600") %> <%# Larger label %> <%= rui_input(name: :field, label: "Field", label_class: "text-base font-bold") %> ``` ### Prefix/Suffix (prefix_class:, suffix_class:) Override prefix and suffix containers: ```erb <%# Custom prefix background %> <%= rui_input(name: :price, prefix_class: "bg-zinc-100") do |input| %> <% input.with_prefix do %>$<% end %> <% end %> <%# Custom suffix color %> <%= rui_input(name: :email, suffix_class: "text-indigo-500") do |input| %> <% input.with_suffix do %> <%= rui_icon(:mail, size: :sm) %> <% end %> <% end %> ``` ### Input Container (container_class:) Override the container that wraps input + prefix/suffix: ```erb <%# Custom container background %> <%= rui_input(name: :search, container_class: "bg-zinc-50 rounded-lg") do |input| %> <% input.with_prefix do %> <%= rui_icon(:search, size: :sm) %> <% end %> <% end %> ``` ### Help Text (help_text_class:) Override the help text styling: ```erb <%# Custom help text color %> <%= rui_input(name: :field, help_text: "Helper info", help_text_class: "text-indigo-500") %> <%# Larger help text %> <%= rui_input(name: :field, help_text: "Important note", help_text_class: "text-sm font-medium") %> ``` ### Combined Example ```erb <%= rui_input( name: :email, type: :email, label: "Email Address", help_text: "We'll never share your email", class: "border-indigo-300 focus:ring-indigo-500", wrapper_class: "max-w-md", label_class: "text-indigo-700 font-semibold", help_text_class: "text-indigo-400 italic" ) %> ``` ## API Reference | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `form` | FormBuilder | nil | Form builder instance (auto-set by f.rui_input) | | `object` | Object/Symbol | nil | Object to bind to | | `method` | Symbol | nil | Attribute name | | `type` | Symbol | :text | Input type (text, email, password, number, tel, url, search) | | `name` | String | auto | Input name attribute | | `value` | String | auto | Input value | | `placeholder` | String | nil | Placeholder text | | `required` | Boolean | false | Show required indicator | | `readonly` | Boolean | false | Make input readonly | | `disabled` | Boolean | false | Disable input | | `pattern` | String | nil | Validation regex pattern | | `maxlength` | Integer | nil | Maximum character length | | `minlength` | Integer | nil | Minimum character length | | `min` | Numeric | nil | Minimum value (number) | | `max` | Numeric | nil | Maximum value (number) | | `step` | Numeric/String | nil | Step value (number, "any" for decimals) | | `autocomplete` | String/Symbol | nil | Autocomplete hint | | `inputmode` | String/Symbol | nil | Virtual keyboard hint | | `list` | String | nil | Datalist ID | | `label` | String | nil | Label text | | `help_text` | String | nil | Help text below input | | `validation` | Boolean | true | Show validation message placeholder (set false for filter inputs) | | `variant` | Symbol | :default | Visual variant (default, filled, outline) | | `color` | Symbol | :primary | Focus ring color | | `size` | Symbol | :base | Size (xs, sm, base, lg, xl) | | `shape` | Symbol | :rounded | Shape (square, rounded, pill) | | `class` | String | nil | Custom classes for input element (overrides defaults) | | `wrapper_class` | String | nil | Custom classes for outer wrapper | | `label_class` | String | nil | Custom classes for label element | | `prefix_class` | String | nil | Custom classes for prefix container | | `suffix_class` | String | nil | Custom classes for suffix container | | `container_class` | String | nil | Custom classes for input container (with prefix/suffix) | | `help_text_class` | String | nil | Custom classes for help text | ## Slots | Slot | Description | |------|-------------| | `prefix` | Leading content (icon or text) | | `suffix` | Trailing content (icon or text) | Note: Password inputs automatically use the suffix slot for the show/hide toggle. ## Accessibility - Labels are associated with inputs via `for`/`id` - `aria-required` added when required - `aria-invalid` added when errors present - `aria-describedby` links to help text and error messages - Error messages have `role="alert"` - Password toggle has `aria-label` --- ## Link # RapidRailsUI Link Component Usage The `Link` component renders an `` element with conditional rendering support and full Turbo integration. It provides Rails-style syntax, underline variants, and context-aware behavior. **Default behavior:** Links are blue and show an underline on hover for clear visual feedback. --- ## Key Features - **Conditional Rendering**, Support for `if:`, `unless:`, and `unless_current:` - **Turbo Integration**, Full support for Turbo Frame, Stream, Method, and Prefetch - **External Links**, Automatic security attributes for external URLs - **Icon Support**, Leading and trailing icons with color override - **Underline Variants**, Four different underline styles - **Active State**, Highlight navigation/tabs with active styling - **Text Truncation**, Auto-truncate long text with tooltips - **SEO Features**, Enhanced rel attributes (sponsored, ugc, nofollow) - **Communication Links**, Shortcuts for email, phone, and SMS links - **Accessibility**, HrefLang, tooltips, and referrer policy support --- ## Basic Usage ```erb <%= rui_link("View Posts", "/posts") %> <%= rui_link("View Posts", "/posts", color: :primary) %> <%= rui_link("/posts") do %> View Posts <% end %> <%= rui_link(post_path(@post), class: "custom-class") do %> Read Full Post <% end %> <%= rui_link(url: "/posts", text: "View Posts", color: :primary) %> ``` **Signature matches Rails `link_to`:** - **Positional (text first):** `rui_link("text", "/url", options)` - **Block (URL first):** `rui_link("/url", options) { "text from block" }` ## Underline Variants Links have four underline styles optimized for different use cases: ```erb <%= rui_link("Hover Underline", "#", variant: :hover_underline) %> <%= rui_link("Underline", "#", variant: :underline) %> <%= rui_link("No Underline", "#", variant: :no_underline) %> <%= rui_link("Animated", "#", variant: :animated_underline) %> ``` **Default:** `:hover_underline` - Shows underline on hover, providing clear visual feedback. ## Colors ### Semantic Colors (Configurable) These colors are configurable via `config/initializers/rapidrailsui.rb`: ```erb <%= rui_link("Primary", "#", color: :primary) %> <%= rui_link("Secondary", "#", color: :secondary) %> <%= rui_link("Accent", "#", color: :accent) %> ``` ### Standard UI Colors ```erb <%= rui_link("Success", "#", color: :success) %> <%= rui_link("Danger", "#", color: :danger) %> <%= rui_link("Warning", "#", color: :warning) %> <%= rui_link("Info", "#", color: :info) %> ``` ### Tailwind Palette Colors All 22 Tailwind color families supported: ```erb <%= rui_link("Blue", "#", color: :blue) %> <%= rui_link("Red", "#", color: :red) %> <%= rui_link("Emerald", "#", color: :emerald) %> <%= rui_link("Purple", "#", color: :purple) %> ``` **Default:** `:blue` - Standard web link color. ## Sizes Five text size options: ```erb <%= rui_link("Extra Small", "#", size: :xs) %> <%= rui_link("Small", "#", size: :sm) %> <%= rui_link("Base", "#", size: :base) %> <%= rui_link("Large", "#", size: :lg) %> <%= rui_link("Extra Large", "#", size: :xl) %> ``` ## With Icons Icons automatically inherit the link color. Supports both leading and trailing positions: ```erb <%= rui_link("Download", "#") do |link| %> <% link.with_icon(:download) %> <% end %> <%= rui_link("Next Page", "#") do |link| %> <% link.with_icon(:arrow_right, position: :trailing) %> <% end %> <% link.with_icon(:heart) %> <% link.with_icon(name: :heart) %> <% link.with_icon(:arrow_right, position: :trailing) %> ``` **Note:** Uses Lucide icons (1500+ available). Requires `lucide-rails` gem. ### Icon Color Override Icons automatically inherit the link's color, but you can override with a custom color: ```erb <%# Icon inherits link color (default behavior) %> <%= rui_link("Posts", "/posts", color: :blue) do |link| %> <% link.with_icon(:heart) %> <%# Icon will be blue %> <% end %> <%# Icon with custom color (overrides inheritance) %> <%= rui_link("Posts", "/posts", color: :blue) do |link| %> <% link.with_icon(:heart, color: :red) %> <%# Blue link with red heart icon %> <% end %> <%# Useful for status indicators %> <%= rui_link("Notifications", "/notifications") do |link| %> <% link.with_icon(:bell, color: :red) %> <%# Default link color with red notification icon %> <% end %> ``` **Note:** When you provide an explicit `color:` parameter to the icon, it uses standalone mode and renders with its own color instead of inheriting from the link. ## Social Media Links Social media links typically combine an icon with an external URL. Use the `url:` keyword argument and `external: true` for external links: ### Basic Social Media Link Pattern ```erb <%= rui_link(url: "https://twitter.com/username", color: :primary, external: true) do |link| %> <% link.with_icon(:twitter) %> <% end %> <%= rui_link(url: "https://twitter.com/username", color: :primary, external: true, title: "Follow us on Twitter") do |link| %> <% link.with_icon(:twitter) %> <% end %> <%= rui_link(url: "https://github.com/username", external: true) do |link| %> <% link.with_icon(:github) %> GitHub <% end %> ``` **Key Points:** - **`url:` is a keyword argument**, not positional (use `url: "..."`, not as the first argument) - **`external: true`** adds security attributes and indicates external navigation - **Block syntax required for icons**: `do |link|` then `link.with_icon()` - **`title:` attribute** improves accessibility for icon-only links ### Social Media Link Group Pattern ```erb
<%= rui_link(url: @social_links[:twitter], color: :primary, external: true, title: "Twitter") do |link| %> <% link.with_icon(:twitter) %> <% end %> <%= rui_link(url: @social_links[:linkedin], color: :primary, external: true, title: "LinkedIn") do |link| %> <% link.with_icon(:linkedin) %> <% end %> <%= rui_link(url: @social_links[:github], color: :primary, external: true, title: "GitHub") do |link| %> <% link.with_icon(:github) %> <% end %> <%= rui_link(url: @social_links[:instagram], color: :primary, external: true, title: "Instagram") do |link| %> <% link.with_icon(:instagram) %> <% end %>
``` ### Icon-Only vs Icon+Text ```erb <%= rui_link(url: "https://twitter.com/username", external: true, title: "Twitter") do |link| %> <% link.with_icon(:twitter) %> <% end %> <%= rui_link(url: "https://twitter.com/username", external: true) do |link| %> <% link.with_icon(:twitter) %> Twitter <% end %> <%= rui_link(url: "https://twitter.com/username", external: true) do |link| %> Follow us <% link.with_icon(:arrow_right, position: :trailing) %> <% end %> ``` ### Styling Social Media Links ```erb <%= rui_link(url: "https://twitter.com", color: :primary, external: true) do |link| %> <% link.with_icon(:twitter) %> <% end %> <%= rui_link(url: "https://github.com", external: true) do |link| %> <% link.with_icon(:github, color: :zinc) %> <% end %> <%= rui_link(url: "https://linkedin.com", variant: :no_underline, external: true) do |link| %> <% link.with_icon(:linkedin) %> <% end %> ``` ### Complete Example: Social Media Footer ```erb

Follow Us

<% @social_links.each do |platform, url| %> <%= rui_link(url: url, external: true, title: platform.to_s.titleize) do |link| %> <% link.with_icon(platform.to_sym) %> <% end %> <% end %>
``` ### API Reference: url: Keyword Argument The `rui_link` component accepts both positional and keyword argument syntax: ```erb <%= rui_link("Click me", "/path", color: :primary) %> <%= rui_link(url: "/path", color: :primary) do %> Content with icon or custom HTML <% end %> <%= rui_link(url: "/path", text: "Click me", color: :primary) %> ``` **When to use `url:` keyword argument:** - Using block syntax for icons - Using block syntax for custom HTML content - Need to pass many options (more readable than positional) - Icon-only links (must use block syntax) **External URL Security:** The `external: true` option automatically adds security attributes: ```erb <%= rui_link(url: "https://external-site.com", external: true) do %> External Site <% end %>
External Site ``` --- ## States ### Disabled State ```erb <%= rui_link("Disabled Link", "#", disabled: true) %> ``` ### Loading State ```erb <%= rui_link("Loading", "#", loading: true) %> ``` ## Conditional Rendering Links can conditionally render as text based on logic: ### If/Unless Conditions ```erb <%= rui_link("Admin Panel", "/admin", if: current_user.admin?) %> <%= rui_link("Posts", "/posts", unless: read_only_mode?) %> <%= rui_link("Feature", "#", if: false) %> ``` ### Unless Current (Navigation) Perfect for navigation menus - renders as text when on current page: ```erb ``` When on the current page, the link renders as muted text instead of a clickable link. ### Real-Life Examples: Post Management ```erb <%# Show different actions based on post state and user permissions %>
<%# Edit link - only for post owner or admin %> <%= rui_link(edit_post_path(@post), "Edit", if: current_user.can_edit?(@post), color: :primary) do |link| link.with_icon(:edit) end %> <%# Publish link - only for unpublished posts %> <%= rui_link(publish_post_path(@post), "Publish", if: !@post.published?, color: :success, turbo_method: :patch) do |link| link.with_icon(:send) end %> <%# View link - renders as text when on current post page %> <%= rui_link(post_path(@post), "View", unless_current: true, color: :blue) %> <%# Premium feature - only for premium users %> <%= rui_link(analytics_post_path(@post), "Analytics", if: current_user.premium?, color: :purple) do |link| link.with_icon(:bar_chart) end %>
``` ## External Links Automatically adds security attributes for external URLs: ```erb <%= rui_link("Visit GitHub", "https://github.com", external: true) %> <%= rui_link("External", "https://example.com", target: "_blank") %> ``` External links automatically show a small icon indicator. ## HTTP Methods For non-GET requests, links automatically wrap in a form: ```erb <%= rui_link("Delete", "/posts/1", method: :delete, color: :danger) %> <%= rui_link("Subscribe", "/subscribe", method: :post, color: :primary) %> ``` **Note:** For RESTful actions, consider using `rui_button_to` instead for better semantics. ## Turbo Integration Full support for Hotwire Turbo features: ### Turbo Methods ```erb <%= rui_link("Delete", "/posts/1", turbo_method: :delete) %> <%= rui_link("Delete", "/posts/1", turbo_method: :delete, turbo_confirm: "Are you sure?") %> ``` ### Turbo Frames ```erb <%= rui_link("New Post", "/posts/new", turbo_frame: "modal") %> <%= rui_link("All Posts", "/posts", turbo_frame: "_top") %> ``` ### Turbo Streams ```erb <%= rui_link("Load More", "/posts", turbo_stream: true) %> ``` ### Turbo Actions ```erb <%= rui_link("Refresh", "/posts", turbo_action: :replace) %> ``` ### Turbo Prefetch Prefetch pages on hover for instant navigation (Turbo 8+ feature): ```erb <%= rui_link("Posts", "/posts", turbo_prefetch: true) %> ``` **How it works:** When a user hovers over the link, Turbo prefetches the page in the background. When they click, the page loads instantly from cache. **Note:** Only use for navigation links, not for forms or actions that change data. ### Real-Life Examples: Post Actions with Turbo ```erb <%# Post index page with Turbo Frames %> <% @posts.each do |post| %>

<%= post.title %>

<%# Edit in modal - loads into turbo frame %> <%= rui_link(edit_post_path(post), "Edit", turbo_frame: "modal", color: :primary) do |link| link.with_icon(:edit) end %> <%# Quick publish/unpublish with Turbo method %> <% if post.published? %> <%= rui_link(unpublish_post_path(post), "Unpublish", turbo_method: :patch, turbo_confirm: "Unpublish this post?", color: :warning) do |link| link.with_icon(:eye_off) end %> <% else %> <%= rui_link(publish_post_path(post), "Publish", turbo_method: :patch, color: :success) do |link| link.with_icon(:send) end %> <% end %> <%# Delete with confirmation %> <%= rui_link(post_path(post), "Delete", turbo_method: :delete, turbo_confirm: "Are you sure? This cannot be undone.", color: :danger) do |link| link.with_icon(:trash_2) end %>
<% end %> <%# Load more posts with Turbo Stream %> <%= rui_link(posts_path(page: @next_page), "Load More Posts", turbo_stream: true, variant: :outline) if @next_page %>
<%# Modal frame for editing %> <%# Content loads here when clicking Edit %> <%# Post detail page with instant navigation %>

<%= @post.title %>

<%# Author card that loads in sidebar frame %> <%= rui_link(user_path(@post.author), "View Author Profile", turbo_frame: "sidebar", color: :blue) %>
``` ## Advanced Features ### Active State Highlight the current/active link in navigation or tabs: ```erb <%= rui_link("Posts", "/posts", active: true) %> ``` **Styling:** Active links are always underlined, font-semibold, and use a darker shade of the color. ### Navigation Variant The `:nav` variant is specifically designed for sidebar and navigation menus. It automatically detects the current page and applies appropriate styling without manual `current_page?` checks: ```erb ``` **Features:** - **Auto-detection**: Automatically detects current page (no need for `current_page?` checks) - **Default color**: Uses `:neutral` (gray) by default for standard navigation - **Color theming**: Can use any color for color-coded navigation sections - **Active state**: Shows background color when active, subtle text color when inactive - **Block-level**: Renders as block with padding, suitable for vertical navigation **Styling:** - **Active**: Background color with white text (e.g., `bg-neutral-600 text-white`) - **Inactive**: Subtle text color with hover background (e.g., `text-neutral-700 hover:bg-neutral-100`) **Real-world example - Sidebar navigation:** ```erb ``` **Comparison with other approaches:** ```erb <%# ❌ Old way - Manual current_page? check with CSS classes %> <%= link_to "Button", docs_button_path, class: "block px-3 py-2 text-sm font-medium #{current_page?(docs_button_path) ? 'bg-zinc-500 text-white' : 'text-zinc-700 hover:bg-zinc-100'} rounded-lg" %> <%# ❌ Using unless_current - Hides link when active %> <%= rui_link("Button", docs_button_path, unless_current: true) %> <%# ✅ Navigation variant - Clean, automatic, and styled for navigation %> <%= rui_link("Button", docs_button_path, variant: :nav) %> ``` **When to use:** - Sidebar navigation menus - Vertical navigation lists - Documentation table of contents - Admin panel navigation - Dashboard sidebars **When NOT to use:** - Horizontal navigation bars (use `active: current_page?(path)` instead) - Inline text links (use `:hover_underline` variant) - Button-style navigation (use button component) ### Tooltips Add tooltips (title attribute) to links: ```erb <%= rui_link("API", "/docs", title: "View API Documentation") %> <%= rui_link("API", "/docs", tooltip: "View API Documentation") %> ``` **Accessibility:** Tooltips provide additional context for screen readers and visual users. ### Text Truncation Automatically truncate long text with automatic title tooltip: ```erb <%= rui_link("/posts/123", @post.very_long_title, truncate: true) %> <%= rui_link("/posts/123", @post.very_long_title, truncate: 50) %> ``` **Auto-tooltip:** When truncated, the full text is automatically shown in a tooltip on hover. ### Enhanced Rel Attributes Full control over link relationships for SEO and security: ```erb <%= rui_link("Sponsor", "https://example.com", rel: "sponsored", external: true) %> <%= rui_link(@comment.website, @comment.author, rel: "ugc nofollow", external: true) %> <%= rui_link("Link", "#", rel: "nofollow sponsored") %> <%= rui_link("Link", "#", rel: ["nofollow", "sponsored"]) %> <%= rui_link("External", "https://example.com", external: true) %> ``` **Common rel values:** - `noopener noreferrer` - Security for external links (automatic with external: true) - `sponsored` - Paid/affiliate links - `ugc` - User-generated content - `nofollow` - Don't follow for SEO ### Scroll Behavior Control scroll behavior for anchor links: ```erb <%= rui_link("Features", "#features", scroll: :smooth) %> <%= rui_link("Back to Top", "#top", scroll: :instant) %> <%= rui_link("Section", "#section", scroll: :auto) %> ``` **Note:** Works with anchor links (href="#section"). Requires modern browser support. ### Multilingual Links (HrefLang) Indicate the language of the linked resource: ```erb <%= rui_link("À propos", "/fr/about", hreflang: "fr") %> <%= rui_link("Acerca de", "/es/about", hreflang: "es") %>
<%= rui_link("English", "/en/page", hreflang: "en") %> <%= rui_link("Français", "/fr/page", hreflang: "fr") %> <%= rui_link("Español", "/es/page", hreflang: "es") %>
``` **SEO Benefit:** Helps search engines understand language variations of your content. ### Referrer Policy Control what referrer information is sent with the link: ```erb <%= rui_link("Private Link", "https://example.com", referrerpolicy: "no-referrer", external: true) %> <%= rui_link("Analytics", "https://analytics.example.com", referrerpolicy: "origin", external: true) %> <%= rui_link("Secure Link", "https://example.com", referrerpolicy: "strict-origin-when-cross-origin", external: true) %> ``` **Common policies:** - `no-referrer` - Don't send any referrer (maximum privacy) - `origin` - Send only origin (https://example.com), not full URL - `strict-origin-when-cross-origin` - Full URL for same-origin, origin for cross-origin ## URL Options ### Query Parameters ```erb <%= rui_link("Search", "/search", params: { q: "rails", sort: "recent" }) %> ``` ### Anchors ```erb <%= rui_link("Features", "/docs", anchor: "features") %> ``` ### Download Attribute ```erb <%= rui_link("Download Report", "/files/report.pdf", download: "report.pdf") %> ``` ### Communication Links `rui_link` provides first-class support for email, phone, and SMS links with all the features of Rails' `mail_to`, `phone_to`, and `sms_to` helpers. #### Email Links (`mail_to` compatibility) Create `mailto:` links with optional subject, body, CC, BCC, and reply-to: ```erb <%# Basic email link %> <%= rui_link(email: "contact@example.com", text: "Email Us") %> <%# With subject %> <%= rui_link(email: "support@example.com", subject: "Support Request", text: "Get Support") %> <%# With subject and body %> <%= rui_link(email: "feedback@example.com", subject: "Product Feedback", body: "I love your product!", text: "Send Feedback") %> <%# With CC (string or array) %> <%= rui_link(email: "team@example.com", cc: "manager@example.com", text: "Email Team") %> <%= rui_link(email: "team@example.com", cc: ["manager@example.com", "ceo@example.com"], text: "Email Leadership") %> <%# With BCC (string or array) %> <%= rui_link(email: "newsletter@example.com", bcc: "archive@example.com", text: "Subscribe") %> <%# With reply-to %> <%= rui_link(email: "noreply@example.com", reply_to: "support@example.com", text: "Contact") %> <%# All options combined %> <%= rui_link(email: "contact@example.com", subject: "Inquiry from website", body: "I have a question about...", cc: "manager@example.com", bcc: ["archive@example.com", "log@example.com"], reply_to: "support@example.com", text: "Contact Sales") %> ``` **Real-world example - Contact form:** ```erb
<%# Sales inquiry with pre-filled subject %> <%= rui_link(email: "sales@example.com", subject: "Sales Inquiry from #{current_user.name}", body: "Hi, I'm interested in...", text: "Contact Sales", color: :primary) do |link| link.with_icon(:mail) end %> <%# Support with CC to manager %> <%= rui_link(email: "support@example.com", subject: "Support Request - Account ##{current_user.id}", cc: "manager@example.com", text: "Get Support", color: :blue) do |link| link.with_icon(:help_circle) end %> <%# Bug report with pre-filled template %> <%= rui_link(email: "bugs@example.com", subject: "[Bug Report] Issue on #{controller_name}##{action_name}", body: "Steps to reproduce:\n1. \n2. \n3. \n\nExpected:\nActual:", text: "Report Bug", color: :danger) do |link| link.with_icon(:bug) end %>
``` #### Phone Links (`phone_to` compatibility) Create `tel:` links with optional country code: ```erb <%# Basic phone link %> <%= rui_link(phone: "555-1234", text: "Call Us") %> <%# With country code (+ is optional, will be normalized) %> <%= rui_link(phone: "555-1234", country_code: "+1", text: "Call US") %> <%= rui_link(phone: "555-1234", country_code: "1", text: "Call US") %> <%# Auto-adds + %> <%# International numbers %> <%= rui_link(phone: "20 7946 0958", country_code: "+44", text: "Call UK Office") %> <%# Number with country code already included (won't duplicate) %> <%= rui_link(phone: "+1-555-1234", text: "Call") %> ``` **Real-world example - Contact info:** ```erb

US Office

<%= rui_link(phone: "555-0100", country_code: "1", text: "Call: +1 (555) 0100", color: :blue) do |link| link.with_icon(:phone) end %>

UK Office

<%= rui_link(phone: "20 7946 0958", country_code: "+44", text: "Call: +44 20 7946 0958", color: :blue) do |link| link.with_icon(:phone) end %>
<%# Emergency support hotline %> <%= rui_link(phone: "911", text: "Emergency Support", color: :danger, size: :lg) do |link| link.with_icon(:phone_call) end %>
``` #### SMS Links (`sms_to` compatibility) Create `sms:` links with optional body and country code: ```erb <%# Basic SMS link %> <%= rui_link(sms: "555-1234", text: "Text Us") %> <%# With pre-filled message body %> <%= rui_link(sms: "555-1234", body: "Hello! I'd like more information", text: "Send SMS") %> <%# With country code %> <%= rui_link(sms: "555-1234", country_code: "+1", text: "Text US") %> <%# Both body and country code %> <%= rui_link(sms: "555-1234", country_code: "1", body: "Quick question about your product", text: "Send Message") %> ``` **Real-world example - Quick actions:** ```erb
<%# Appointment reminder confirmation %> <%= rui_link(sms: "555-DOCTOR", body: "CONFIRM appointment for #{@appointment.date}", text: "Confirm via SMS", variant: :outline) do |link| link.with_icon(:message_square) end %> <%# Customer support quick text %> <%= rui_link(sms: "555-SUPPORT", country_code: "1", body: "Order ##{@order.id} - ", text: "Text Support", color: :blue) do |link| link.with_icon(:message_circle) end %> <%# Marketing opt-in %> <%= rui_link(sms: "555-NEWS", body: "JOIN to receive daily updates", text: "Subscribe via SMS", variant: :soft, color: :purple) %>
``` **Direct URL Usage (Alternative):** You can always use direct `mailto:`, `tel:`, or `sms:` URLs if you prefer: ```erb <%= rui_link("Email Us", "mailto:contact@example.com") %> <%= rui_link("Call Us", "tel:+1-555-1234") %> <%= rui_link("Text Us", "sms:+1-555-1234") %> ``` **Platform Compatibility Notes:** - **Email**: `subject`, `body`, `cc`, `bcc`, `reply-to` work in all major email clients - **Phone**: Country codes work universally with `tel:` protocol - **SMS**: `body` parameter support varies: - ✅ **iOS**: Full support for pre-filled message body - ⚠️ **Android**: Partial support (varies by messaging app) - ❌ **Desktop**: Most desktop browsers don't support SMS links **Priority Order:** If multiple communication parameters are provided, priority is: `url` > `email` > `phone` > `sms` ```erb <%# URL always takes priority %> <%= rui_link("/contact", email: "ignored@example.com", text: "Uses /contact") %> <%# Email takes priority over phone/sms %> <%= rui_link(email: "contact@example.com", phone: "ignored", text: "Uses email") %> <%# Phone takes priority over SMS %> <%= rui_link(phone: "555-1234", sms: "ignored", text: "Uses phone") %> ``` ## Full Width ```erb <%= rui_link("Full Width Link", "#", full_width: true) %> ``` ## Data Attributes ```erb <%= rui_link("Analytics Link", "#", data: { controller: "analytics", action: "click->analytics#track", category: "navigation" }) %> ``` ## Real-World Examples ### Link in Paragraph ```erb

RapidRailsUI is a ViewComponent-based UI library. Learn more in our <%= rui_link("documentation", "/docs") %> or check out the <%= rui_link("source code", "https://github.com", external: true) %> on GitHub.

``` ### Icon Links for Actions ```erb
<%= rui_link("#") do |link| %> <% link.with_icon(:download) %> Download PDF <% end %> <%= rui_link("#") do |link| %> <% link.with_icon(:share_2) %> Share <% end %>
``` ### Call-to-Action Links ```erb <%= rui_link("/signup", size: :lg, color: :primary) do |link| %> Get Started <% link.with_icon(:arrow_right, position: :trailing) %> <% end %> ``` ### Card with Action Link ```erb

Featured Post

Learn how to build amazing Rails applications with our latest tutorial.

<%= rui_link("/posts/1", color: :primary) do |link| %> Read more <% link.with_icon(:arrow_right, position: :trailing) %> <% end %>
``` ### Navigation with Active States ```erb ``` ### List of Related Links ```erb
  • <%= rui_link("/docs/button", variant: :hover_underline) do |link| %> <% link.with_icon(:circle) %> Button Component <% end %>
  • <%= rui_link("/docs/icon", variant: :hover_underline) do |link| %> <% link.with_icon(:circle) %> Icon Component <% end %>
``` ### Footer Links ```erb
Product
  • <%= rui_link("Features", "/features", variant: :hover_underline, color: :zinc) %>
  • <%= rui_link("Pricing", "/pricing", variant: :hover_underline, color: :zinc) %>
``` ## Combining Text Parameter with Block (Icon Slot) You can pass text as a parameter and use the block for the icon slot: ```erb <%= rui_link("Get Started!", "/signup", size: :lg, color: :primary) do |link| %> <% link.with_icon(:arrow_right, position: :trailing) %> <% end %> ``` This pattern is useful when you want cleaner inline syntax while still adding icons. ## Custom Classes ```erb <%= rui_link("Custom", "#", class: "my-custom-class another-class") %> ``` ## Component Architecture The Link component: - Renders `` elements by default - Conditionally renders as `` when conditions fail (if/unless/unless_current) - Wraps in `
` for non-GET HTTP methods - Inherits color context when used in slots (standalone: false) - Supports dark mode automatically - WCAG AA accessible with proper ARIA attributes ## Tips 1. **Use `unless_current: true` for navigation** to automatically highlight the current page 2. **Prefer `:hover_underline` variant** (default) for better UX - users expect hover feedback 3. **Use `external: true`** for external links to add security attributes automatically 4. **Consider `rui_button_to` for actions** (DELETE/POST) instead of links with methods 5. **Icons inherit link color** automatically when used in slots 6. **Loading state** is useful for async actions or when navigating to slow pages 7. **Enable `turbo_prefetch: true` for navigation** to make page loads feel instant 8. **Use `active: true` with `current_page?`** for dynamic navigation highlighting 9. **Truncate long titles** with `truncate: 50` - automatically adds tooltip with full text 10. **Add `rel: "sponsored"` for affiliate links** to comply with SEO best practices 11. **Use `scroll: :smooth` for anchor links** to create smooth scrolling effects 12. **Set `referrerpolicy` for external links** when privacy is a concern --- ## Live Search # LiveSearch Component Live search input that queries server as you type (debounced). Results update via Turbo Frame, proving Rails can do what React does, but simpler. --- ## Table of Contents - [Basic Usage](#basic-usage) - [Parameters Reference](#parameters-reference) - [Required Parameters](#required-parameters) - [Size Options](#size-options) - [Shape Options](#shape-options) - [Placeholder](#placeholder) - [Debounce](#debounce) - [Minimum Length](#minimum-length) - [Parameter Name](#parameter-name) - [Clear Button](#clear-button) - [Loading Indicator](#loading-indicator) - [Autofocus](#autofocus) - [Disabled State](#disabled-state) - [Search Button](#search-button) - [Keyboard Shortcut](#keyboard-shortcut) - [Voice Search](#voice-search) - [Scope/Category Dropdown](#scopecategory-dropdown) - [Recent Searches](#recent-searches) - [Modal Mode](#modal-mode) - [Modal Branding](#modal-branding) - [Keyboard Navigation](#keyboard-navigation) - [Slots](#slots) - [Custom Styling](#custom-styling) - [Controller Setup](#controller-setup) - [Stimulus Controller Reference](#stimulus-controller-reference) - [Accessibility](#accessibility) - [All Features Combined](#all-features-combined) --- ## Basic Usage ```erb <%= rui_live_search url: search_posts_path, turbo_frame: "search_results" %> <%= render @posts %> ``` --- ## Parameters Reference | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `url` | String | **required** | Search endpoint URL | | `turbo_frame` | String | **required** | Target Turbo Frame ID for results | | `placeholder` | String | nil | Placeholder text for the input | | `debounce` | Integer | 300 | Debounce delay in milliseconds | | `min_length` | Integer | 1 | Minimum characters before search triggers | | `param_name` | Symbol | :q | Query parameter name | | `size` | Symbol | :base | Size variant (:sm, :base, :lg) | | `shape` | Symbol | :rounded | Shape variant (:square, :rounded, :pill) | | `clear_button` | Boolean | false | Show clear button when input has value | | `loading_indicator` | Boolean | false | Show loading spinner during request | | `autofocus` | Boolean | false | Focus input on page load | | `disabled` | Boolean | false | Disable the search input | | `search_button` | Boolean | false | Show search submit button | | `search_button_text` | String | "Search" | Text for search button | | `shortcut` | String | nil | Global keyboard shortcut key (e.g., "k" for ⌘K/Ctrl+K) | | `shortcut_hint` | Boolean | true (when shortcut set) | Show shortcut hint badge inside input | | `voice_search` | Boolean | false | Show voice search microphone button | | `scope` | Array | [] | Array of {value:, label:} hashes for scope dropdown | | `scope_param` | Symbol | :scope | Query parameter name for scope | | `scope_default` | String | nil | Default selected scope value | | `recent_searches` | Boolean | false | Enable recent searches dropdown | | `recent_searches_limit` | Integer | 5 | Max recent searches to store | | `recent_searches_key` | String | 'rui_recent_searches' | LocalStorage key for recent searches | | `modal` | Boolean | false | Enable modal mode - search opens in dialog | | `modal_size` | Symbol | :lg | Modal dialog size (:sm, :md, :lg, :xl) | | `show_branding` | Boolean | true | Show "Search by RapidRailsUI" branding in modal footer | | `brand_name` | String | "RapidRailsUI" | Brand name to display in footer | | `brand_logo` | String | nil | Path to brand logo image (defaults to gem's built-in logo) | | `name` | String | nil | HTML name attribute (defaults to param_name) | | `id` | String | nil | HTML id attribute for the input | | `aria_label` | String | nil | Accessible label for screen readers | --- ## Required Parameters ### `url` (required) The search endpoint URL that receives GET requests with the search query. ```erb <%# Using a named route %> <%= rui_live_search url: search_posts_path, turbo_frame: "results" %> <%# Using a string path %> <%= rui_live_search url: "/api/search", turbo_frame: "results" %> <%# Using a route with parameters %> <%= rui_live_search url: search_posts_path(category: "tech"), turbo_frame: "results" %> ``` ### `turbo_frame` (required) The ID of the Turbo Frame that will receive the search results. ```erb <%= rui_live_search url: search_path, turbo_frame: "search_results" %> <%# The Turbo Frame must exist on the page %> <%= render @results %> ``` --- ## Size Options Three sizes available: `:sm`, `:base` (default), `:lg` | Size | Height | Use Case | |------|--------|----------| | `:sm` | h-8 (32px) | Compact layouts, inline search, sidebars | | `:base` | h-10 (40px) | Default, most use cases | | `:lg` | h-11 (44px) | Prominent search, hero sections, landing pages | ```erb <%# Small - compact search %> <%= rui_live_search url: search_path, turbo_frame: "results", size: :sm, placeholder: "Quick search..." %> <%# Base (default) - standard search %> <%= rui_live_search url: search_path, turbo_frame: "results", size: :base, placeholder: "Search..." %> <%# Large - prominent search %> <%= rui_live_search url: search_path, turbo_frame: "results", size: :lg, placeholder: "What are you looking for?" %> ``` --- ## Shape Options Three shapes available: `:square`, `:rounded` (default), `:pill` | Shape | Border Radius | Use Case | |-------|---------------|----------| | `:square` | 0 | Sharp corners, minimal design | | `:rounded` | rounded-lg | Default, balanced look | | `:pill` | rounded-full | Soft, modern look | ```erb <%# Square - sharp corners %> <%= rui_live_search url: search_path, turbo_frame: "results", shape: :square %> <%# Rounded (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", shape: :rounded %> <%# Pill - fully rounded %> <%= rui_live_search url: search_path, turbo_frame: "results", shape: :pill %> ``` **Combined with size:** ```erb <%# Large pill-shaped search %> <%= rui_live_search url: search_path, turbo_frame: "results", size: :lg, shape: :pill, placeholder: "Search everything..." %> ``` --- ## Placeholder Custom placeholder text displayed when input is empty. ```erb <%# Default (no placeholder) %> <%= rui_live_search url: search_path, turbo_frame: "results" %> <%# Custom placeholder %> <%= rui_live_search url: search_path, turbo_frame: "results", placeholder: "Search posts, users, tags..." %> <%# Contextual placeholder %> <%= rui_live_search url: search_products_path, turbo_frame: "products", placeholder: "Search by name, SKU, or category" %> ``` --- ## Debounce Delay (in milliseconds) before search triggers after user stops typing. Prevents excessive server requests. ```erb <%# Default: 300ms %> <%= rui_live_search url: search_path, turbo_frame: "results", debounce: 300 %> <%# Fast response: 150ms %> <%= rui_live_search url: search_path, turbo_frame: "results", debounce: 150 %> <%# Slower for expensive queries: 500ms %> <%= rui_live_search url: heavy_search_path, turbo_frame: "results", debounce: 500 %> <%# Instant (no debounce): 0ms %> <%= rui_live_search url: search_path, turbo_frame: "results", debounce: 0 %> ``` --- ## Minimum Length Minimum characters required before search triggers. Useful to prevent searches on single characters. ```erb <%# Default: 1 character %> <%= rui_live_search url: search_path, turbo_frame: "results", min_length: 1 %> <%# Require at least 2 characters %> <%= rui_live_search url: search_path, turbo_frame: "results", min_length: 2 %> <%# Require at least 3 characters (for large datasets) %> <%= rui_live_search url: search_path, turbo_frame: "results", min_length: 3, placeholder: "Enter 3+ characters to search..." %> ``` --- ## Parameter Name The query parameter name sent to the server. Default is `:q`. ```erb <%# Default: params[:q] %> <%= rui_live_search url: search_path, turbo_frame: "results", param_name: :q %> <%# Server receives: /search?q=hello %> <%# Custom: params[:query] %> <%= rui_live_search url: search_path, turbo_frame: "results", param_name: :query %> <%# Server receives: /search?query=hello %> <%# Custom: params[:search_term] %> <%= rui_live_search url: search_path, turbo_frame: "results", param_name: :search_term %> <%# Server receives: /search?search_term=hello %> ``` --- ## Clear Button Show an X button to clear the search input. Appears only when input has a value. ```erb <%# Without clear button (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", clear_button: false %> <%# With clear button %> <%= rui_live_search url: search_path, turbo_frame: "results", clear_button: true %> ``` **Behavior:** - Hidden when input is empty - Shown when input has value - Click to clear input and reset results - Keyboard: Escape key also clears input --- ## Loading Indicator Show a spinning loader while search request is in progress. ```erb <%# Without loading indicator (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", loading_indicator: false %> <%# With loading indicator %> <%= rui_live_search url: search_path, turbo_frame: "results", loading_indicator: true %> <%# Combined with clear button %> <%= rui_live_search url: search_path, turbo_frame: "results", clear_button: true, loading_indicator: true %> ``` **Behavior:** - Hidden by default - Shows when search request starts - Hides when Turbo Frame loads - Temporarily hides clear button while loading --- ## Autofocus Automatically focus the search input when page loads. ```erb <%# Without autofocus (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", autofocus: false %> <%# With autofocus %> <%= rui_live_search url: search_path, turbo_frame: "results", autofocus: true %> ``` **Use cases:** - Search-focused pages where search is the primary action - Modal search dialogs - Landing pages with prominent search --- ## Disabled State Disable the search input to prevent user interaction. ```erb <%# Enabled (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", disabled: false %> <%# Disabled %> <%= rui_live_search url: search_path, turbo_frame: "results", disabled: true %> <%# Conditionally disabled %> <%= rui_live_search url: search_path, turbo_frame: "results", disabled: !current_user.can_search? %> ``` **When disabled:** - Input cannot be focused or edited - Search will not trigger - Clear button will not respond - Visual styling shows disabled state (opacity) - Keyboard shortcuts are ignored --- ## Search Button Add a submit button attached to the right of the input. ```erb <%# Without search button (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", search_button: false %> <%# With search button (default text "Search") %> <%= rui_live_search url: search_path, turbo_frame: "results", search_button: true %> <%# With custom button text %> <%= rui_live_search url: search_path, turbo_frame: "results", search_button: true, search_button_text: "Find" %> <%# With custom button text - Go %> <%= rui_live_search url: search_path, turbo_frame: "results", search_button: true, search_button_text: "Go" %> <%# With icon-like text %> <%= rui_live_search url: search_path, turbo_frame: "results", search_button: true, search_button_text: "→" %> ``` **Behavior:** - Button inherits component's shape (rounded, pill, square) - Clicking submits the search immediately (bypasses debounce) - Disabled when component is disabled --- ## Keyboard Shortcut Enable global keyboard shortcut to focus/open search. ```erb <%# No shortcut (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", shortcut: nil %> <%# ⌘K / Ctrl+K shortcut %> <%= rui_live_search url: search_path, turbo_frame: "results", shortcut: "k" %> <%# ⌘/ / Ctrl+/ shortcut %> <%= rui_live_search url: search_path, turbo_frame: "results", shortcut: "/" %> <%# With shortcut hint hidden %> <%= rui_live_search url: search_path, turbo_frame: "results", shortcut: "k", shortcut_hint: false %> ``` **Behavior:** - **Inline mode:** Shortcut focuses and selects input text - **Modal mode:** Shortcut opens the modal dialog - Hint badge shows ⌘K (Mac) or Ctrl+K (Windows/Linux) - Hint hides when user starts typing - Works from anywhere on the page **Note:** Shortcut only works in modal mode to avoid conflicts with multiple inline searches. --- ## Voice Search Enable microphone button for voice-to-text search using Web Speech API. ```erb <%# Without voice search (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", voice_search: false %> <%# With voice search %> <%= rui_live_search url: search_path, turbo_frame: "results", voice_search: true %> <%# Combined with other features %> <%= rui_live_search url: search_path, turbo_frame: "results", voice_search: true, clear_button: true, loading_indicator: true %> ``` **How it works:** 1. Click microphone button to start voice recognition 2. Button shows red pulse animation while listening 3. Spoken text automatically fills the input 4. Search triggers when voice recognition ends **Browser support:** - Chrome, Edge, Safari (desktop and mobile) - Firefox requires flag (`media.webspeech.recognition.enable`) - Graceful fallback if unsupported **Events:** - `live-search:voice-started` - Recognition started - `live-search:voice-ended` - Recognition ended - `live-search:voice-unsupported` - Browser doesn't support Speech API --- ## Scope/Category Dropdown Add a filter dropdown before the search input to search within categories. ```erb <%# Basic scope dropdown %> <%= rui_live_search url: search_path, turbo_frame: "results", scope: [ { value: "all", label: "All" }, { value: "posts", label: "Posts" }, { value: "users", label: "Users" } ] %> <%# With default selection %> <%= rui_live_search url: search_path, turbo_frame: "results", scope: [ { value: "all", label: "All" }, { value: "posts", label: "Posts" }, { value: "users", label: "Users" }, { value: "comments", label: "Comments" } ], scope_default: "posts" %> <%# With custom parameter name %> <%= rui_live_search url: search_path, turbo_frame: "results", scope: [ { value: "all", label: "All Categories" }, { value: "tech", label: "Technology" }, { value: "design", label: "Design" }, { value: "business", label: "Business" } ], scope_param: :category, scope_default: "all" %> ``` **Server receives:** `/search?q=hello&category=tech` **Controller example:** ```ruby def index @results = case params[:category] when "posts" then Post.search(params[:q]) when "users" then User.search(params[:q]) when "comments" then Comment.search(params[:q]) else search_all(params[:q]) end end ``` **Behavior:** - Changing scope immediately triggers new search - Scope inherits component's shape styling --- ## Recent Searches Enable dropdown showing recent search history stored in browser's LocalStorage. ```erb <%# Without recent searches (default) %> <%= rui_live_search url: search_path, turbo_frame: "results", recent_searches: false %> <%# With recent searches (default limit: 5) %> <%= rui_live_search url: search_path, turbo_frame: "results", recent_searches: true %> <%# With custom limit %> <%= rui_live_search url: search_path, turbo_frame: "results", recent_searches: true, recent_searches_limit: 10 %> <%# With custom storage key %> <%= rui_live_search url: search_path, turbo_frame: "results", recent_searches: true, recent_searches_key: "my_app_searches" %> <%# Full configuration %> <%= rui_live_search url: search_path, turbo_frame: "results", recent_searches: true, recent_searches_limit: 8, recent_searches_key: "blog_searches" %> ``` **How it works:** 1. Open modal (or focus empty inline input) → recent searches dropdown appears 2. Click a recent search → fills input and triggers search 3. New searches automatically saved on form submit 4. "Clear recent searches" button removes all history **Multiple search boxes with separate histories:** ```erb <%# Global search %> <%= rui_live_search url: search_path, turbo_frame: "global_results", recent_searches: true, recent_searches_key: "global_searches" %> <%# Product search %> <%= rui_live_search url: products_search_path, turbo_frame: "product_results", recent_searches: true, recent_searches_key: "product_searches" %> <%# User search %> <%= rui_live_search url: users_search_path, turbo_frame: "user_results", recent_searches: true, recent_searches_key: "user_searches" %> ``` **Events:** - `live-search:recent-selected` - Recent search item clicked - `live-search:recent-cleared` - History cleared --- ## Modal Mode Enable modal mode to show search in a centered dialog (command palette / spotlight search UX). ```erb <%# Without modal (default - inline mode) %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: false %> <%# With modal %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true %> <%# Modal with keyboard shortcut %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, shortcut: "k" %> <%# Modal with custom size %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, modal_size: :xl %> ``` **Modal sizes:** | Size | Width | Use Case | |------|-------|----------| | `:sm` | max-w-md | Compact modal | | `:md` | max-w-lg | Medium modal | | `:lg` | max-w-2xl | Default, most use cases | | `:xl` | max-w-4xl | Large results display | ```erb <%# Small modal %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, modal_size: :sm %> <%# Large modal %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, modal_size: :lg %> <%# Extra large modal %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, modal_size: :xl %> ``` **How it works:** 1. Trigger button appears showing "Search..." and shortcut hint 2. Click button or press ⌘K to open modal 3. Input is auto-focused when modal opens 4. If recent searches exist and input is empty → recent searches shown 5. As user types → recent hides, results show 6. Press Escape or click backdrop to close **Full modal example:** ```erb <%= rui_live_search url: docs_search_path, turbo_frame: "docs_results", modal: true, modal_size: :lg, shortcut: "k", placeholder: "Search documentation...", debounce: 150, min_length: 2, clear_button: true, loading_indicator: true, recent_searches: true, recent_searches_limit: 5, aria_label: "Search documentation" %> ``` --- ## Modal Branding In modal mode, "Search by LOGO RapidRailsUI" branding appears in the footer. ```erb <%# Default branding (shows RapidRailsUI) %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true %> <%# Custom brand name %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, brand_name: "MyApp" %> <%# Custom brand logo %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, brand_logo: "myapp/logo.png" %> <%# Custom brand name and logo %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, brand_name: "Acme Search", brand_logo: "acme/search-logo.svg" %> <%# Hide branding completely %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, show_branding: false %> ``` **Note:** Branding only appears in modal mode. Inline mode never shows branding. --- ## Keyboard Navigation Full keyboard navigation support for both recent searches and search results. **Navigation keys:** | Key | Action | |-----|--------| | `↑` / `↓` | Navigate through items | | `Enter` | Select highlighted item | | `Escape` | Clear input (inline) or close modal | | `⌘K` / `Ctrl+K` | Open modal (when shortcut configured) | **Navigation behavior:** - **Input empty + recent searches visible:** Arrow keys navigate recent items - **Input has value + results visible:** Arrow keys navigate search results - **Enter:** Selects highlighted item (triggers click) - Highlighted items scroll into view automatically **Example with full keyboard support:** ```erb <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, shortcut: "k", recent_searches: true, clear_button: true %> ``` --- ## Slots ### Empty State Slot Render custom content when no results are found. ```erb <%# Basic empty state %> <%= rui_live_search url: search_path, turbo_frame: "results" do |search| %> <% search.with_empty_state do %>

No results found

<% end %> <% end %> <%# Detailed empty state %> <%= rui_live_search url: search_path, turbo_frame: "results" do |search| %> <% search.with_empty_state do %>

No results found

Try adjusting your search or filter to find what you're looking for.

<% end %> <% end %> ``` ### Trigger Slot (Modal Mode) Customize the modal trigger button content. ```erb <%# Default trigger (shows "Search..." with shortcut hint) %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, shortcut: "k" %> <%# Custom trigger %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true, shortcut: "k" do |search| %> <% search.with_trigger do %>
<%= rui_icon :search, size: :sm, class: "text-zinc-400" %> Search documentation... ⌘K
<% end %> <% end %> <%# Minimal trigger %> <%= rui_live_search url: search_path, turbo_frame: "results", modal: true do |search| %> <% search.with_trigger do %> <%= rui_icon :search, size: :base, class: "text-zinc-500 hover:text-zinc-700" %> <% end %> <% end %> ``` --- ## Custom Styling Add custom CSS classes to the component wrapper. ```erb <%# Custom width %> <%= rui_live_search url: search_path, turbo_frame: "results", class: "max-w-md" %> <%# Centered %> <%= rui_live_search url: search_path, turbo_frame: "results", class: "max-w-lg mx-auto" %> <%# Full width %> <%= rui_live_search url: search_path, turbo_frame: "results", class: "w-full" %> <%# With margin %> <%= rui_live_search url: search_path, turbo_frame: "results", class: "max-w-xl mx-auto my-8" %> ``` **HTML attributes:** ```erb <%# Custom ID %> <%= rui_live_search url: search_path, turbo_frame: "results", id: "main-search" %> <%# Custom name %> <%= rui_live_search url: search_path, turbo_frame: "results", name: "search_query" %> <%# ARIA label for accessibility %> <%= rui_live_search url: search_path, turbo_frame: "results", aria_label: "Search blog posts" %> ``` --- ## Controller Setup The search endpoint should respond to GET requests and render results in a Turbo Frame. **Basic controller:** ```ruby # app/controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = Post.all if params[:q].present? @posts = @posts.where("title ILIKE ?", "%#{params[:q]}%") end # Turbo automatically excludes layout for frame requests end end ``` **With scope/category:** ```ruby # app/controllers/search_controller.rb class SearchController < ApplicationController def index query = params[:q] @results = case params[:scope] when "posts" Post.search(query) when "users" User.search(query) when "comments" Comment.search(query) else search_all(query) end end private def search_all(query) { posts: Post.search(query).limit(5), users: User.search(query).limit(5), comments: Comment.search(query).limit(5) } end end ``` **View with Turbo Frame:** ```erb <%# app/views/posts/index.html.erb %> <%= rui_live_search url: posts_path, turbo_frame: "posts_list", placeholder: "Search posts...", clear_button: true, loading_indicator: true %> <% if @posts.any? %> <% @posts.each do |post| %> <%= render post %> <% end %> <% else %>

No posts found

<% end %>
``` --- ## Stimulus Controller Reference The component uses `live-search` Stimulus controller. ### Targets | Target | Description | |--------|-------------| | `form` | The form element | | `input` | The search input | | `clearButton` | Clear button (X) | | `loadingIndicator` | Loading spinner | | `searchButton` | Submit button | | `shortcutHint` | Shortcut badge container | | `shortcutText` | Shortcut text element | | `emptyState` | Empty state slot container | | `voiceButton` | Voice search button | | `scopeSelect` | Scope dropdown | | `recentDropdown` | Recent searches dropdown | | `recentList` | Recent searches list | | `recentClear` | Clear recent button | | `dialog` | Modal dialog element | | `modalTrigger` | Modal trigger button | | `resultsContainer` | Results container (modal) | | `modalEmptyState` | Modal empty state | ### Values | Value | Type | Default | Description | |-------|------|---------|-------------| | `debounce` | Number | 300 | Debounce delay in ms | | `minLength` | Number | 1 | Min characters to trigger | | `disabled` | Boolean | false | Disabled state | | `shortcut` | String | "" | Keyboard shortcut key | | `recentSearches` | Boolean | false | Enable recent searches | | `recentLimit` | Number | 5 | Max recent searches | | `recentKey` | String | "rui_recent_searches" | LocalStorage key | | `modal` | Boolean | false | Enable modal mode | ### Events | Event | Description | |-------|-------------| | `live-search:before-search` | Before form submits (cancelable) | | `live-search:after-search` | After form submitted | | `live-search:cleared` | After input cleared | | `live-search:voice-started` | Voice recognition started | | `live-search:voice-ended` | Voice recognition ended | | `live-search:voice-unsupported` | Browser doesn't support Speech API | | `live-search:recent-selected` | Recent search item clicked | | `live-search:recent-cleared` | History cleared | | `live-search:modal-opened` | Modal opened | | `live-search:modal-closed` | Modal closed | | `live-search:shortcut-triggered` | Keyboard shortcut pressed | **Listening to events:** ```javascript // Listen for search events document.addEventListener("live-search:before-search", (event) => { console.log("Searching for:", event.detail.query) }) document.addEventListener("live-search:after-search", (event) => { console.log("Search completed:", event.detail.query) }) // Cancel a search document.addEventListener("live-search:before-search", (event) => { if (event.detail.query.length < 3) { event.preventDefault() // Cancels the search } }) ``` --- ## Accessibility The component follows WAI-ARIA best practices. ### ARIA Attributes - `role="search"` - Landmark for screen reader navigation (inline mode) - `aria-label` - Custom accessible name - `aria-controls` - Points to the target Turbo Frame - `aria-autocomplete="list"` - Indicates search suggests results - `aria-live="polite"` - Announces loading state and results ### Semantic HTML - Uses native `` - Form uses GET method for bookmarkable URLs - Native `` element for modal ### Keyboard Support - Full keyboard navigation (arrows, enter, escape) - Focus trapping in modal mode - Escape closes modal or clears input ### Recommended Results Markup ```erb <%= rui_live_search url: search_path, turbo_frame: "results", aria_label: "Search posts" %> <%= render @results %> ``` --- ## All Features Combined ```erb <%= rui_live_search url: search_path, turbo_frame: "results", placeholder: "Search everything...", debounce: 200, min_length: 2, param_name: :q, size: :lg, shape: :rounded, clear_button: true, loading_indicator: true, autofocus: false, disabled: false, search_button: false, shortcut: "k", shortcut_hint: true, voice_search: true, scope: [ { value: "all", label: "All" }, { value: "posts", label: "Posts" }, { value: "users", label: "Users" }, { value: "comments", label: "Comments" } ], scope_param: :category, scope_default: "all", recent_searches: true, recent_searches_limit: 8, recent_searches_key: "app_searches", modal: true, modal_size: :lg, show_branding: true, brand_name: "RapidRailsUI", brand_logo: nil, id: "main-search", aria_label: "Search" do |search| %> <% search.with_empty_state do %>

No results found

Try different keywords

<% end %> <% search.with_trigger do %>
<%= rui_icon :search, size: :sm, class: "text-zinc-400" %> Search... ⌘K
<% end %> <% end %> <%= render @results %> ``` --- ## Pagination # Pagination Component A flexible pagination component designed for seamless Pagy gem integration while supporting standalone usage. ## Basic Usage with Pagy The simplest way to use pagination is with the Pagy gem: ```erb <%# Controller: @pagy, @posts = pagy(Post.all) %> <%= rui_pagination(pagy: @pagy) %> ``` ## Standalone Usage For custom pagination without Pagy: ```erb <%= rui_pagination( current_page: 5, total_pages: 20, total_count: 200, base_url: posts_path ) %> ``` ## Variants ### Simple (Previous/Next Only) Minimal pagination with just previous and next buttons: ```erb <%= rui_pagination(pagy: @pagy, variant: :simple) %> ``` ### Numbered (Default) Shows page numbers with previous/next: ```erb <%= rui_pagination(pagy: @pagy, variant: :numbered) %> ``` ### Full (First/Last Included) Complete navigation with first, previous, pages, next, and last: ```erb <%= rui_pagination(pagy: @pagy, variant: :full) %> ``` ### Compact (Minimal) Minimal "Page X of Y" display with prev/next buttons: ```erb <%= rui_pagination(pagy: @pagy, variant: :compact) %> ``` ## Icon-Only Mode Hide button text, show only chevron icons: ```erb <%= rui_pagination(pagy: @pagy, icon_only: true) %> <%= rui_pagination(pagy: @pagy, variant: :simple, icon_only: true) %> ``` ## Alignment Control horizontal alignment of pagination: ```erb <%= rui_pagination(pagy: @pagy, align: :left) %> <%= rui_pagination(pagy: @pagy, align: :center) %> <%= rui_pagination(pagy: @pagy, align: :right) %> <%= rui_pagination(pagy: @pagy, align: :between) %> <%# Default %> ``` ## Page Jumper Add go-to-page input field: ```erb <%= rui_pagination(pagy: @pagy, show_jumper: true) %> <%# Compact with jumper - minimal but powerful %> <%= rui_pagination(pagy: @pagy, variant: :compact, show_jumper: true) %> ``` ## With Turbo Frame (AJAX Pagination) Enable AJAX pagination with Turbo Frame: ```erb <%= turbo_frame_tag "posts" do %>
<%= render @posts %>
<%= rui_pagination( pagy: @pagy, turbo_frame: "posts" ) %> <% end %> ``` ## With Per-Page Selector Allow users to choose how many items per page: ```erb <%= rui_pagination( pagy: @pagy, show_per_page: true, per_page_options: [10, 25, 50, 100] ) %> ``` ```ruby # Controller def index items = params[:per_page]&.to_i || 25 @pagy, @posts = pagy(Post.all, items: items) end ``` ## Without Info Text Hide the "Showing X-Y of Z" info: ```erb <%= rui_pagination(pagy: @pagy, show_info: false) %> ``` ## Sizes ```erb <%= rui_pagination(pagy: @pagy, size: :xs) %> <%= rui_pagination(pagy: @pagy, size: :sm) %> <%= rui_pagination(pagy: @pagy, size: :base) %> <%# Default %> <%= rui_pagination(pagy: @pagy, size: :lg) %> <%= rui_pagination(pagy: @pagy, size: :xl) %> ``` ## Shapes ```erb <%= rui_pagination(pagy: @pagy, shape: :rounded) %> <%# Default %> <%= rui_pagination(pagy: @pagy, shape: :square) %> <%= rui_pagination(pagy: @pagy, shape: :pill) %> ``` ## Colors The active page button can be styled with any color: ```erb <%= rui_pagination(pagy: @pagy, color: :primary) %> <%# Default %> <%= rui_pagination(pagy: @pagy, color: :blue) %> <%= rui_pagination(pagy: @pagy, color: :emerald) %> <%= rui_pagination(pagy: @pagy, color: :purple) %> ``` ## Custom Window Sizes Control how many page links appear: ```erb <%# Show 3 pages on each side of current %> <%= rui_pagination(pagy: @pagy, window: 3) %> <%# Show 2 pages at edges (first/last) %> <%= rui_pagination(pagy: @pagy, outer_window: 2) %> ``` ## Custom Button Text Customize navigation button labels: ```erb <%= rui_pagination( pagy: @pagy, prev_text: "← Back", next_text: "Forward →", first_text: "Start", last_text: "End" ) %> ``` ## In Table Component Pagination integrates directly with the Table component: ```erb <%= rui_table(pagy: @pagy, turbo_frame: "users") do |table| %> <% table.with_column(key: :name, label: "Name") %> <% 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 %> ``` ## Complete Example Full-featured pagination with all options: ```erb <%= rui_pagination( pagy: @pagy, variant: :full, size: :base, shape: :rounded, color: :primary, turbo_frame: "content", show_info: true, show_per_page: true, per_page_options: [10, 25, 50, 100], window: 2, outer_window: 1 ) %> ``` ## Props Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `pagy` | Pagy | nil | Pagy object for automatic configuration | | `current_page` | Integer | 1 | Current page (if no pagy) | | `total_pages` | Integer | 1 | Total pages (if no pagy) | | `total_count` | Integer | nil | Total items (for info display) | | `variant` | Symbol | :numbered | Display variant (:simple, :numbered, :full, :compact) | | `size` | Symbol | :base | Size variant (:xs, :sm, :base, :lg, :xl) | | `shape` | Symbol | :rounded | Button shape (:rounded, :square, :pill) | | `color` | Symbol | :primary | Active page color | | `align` | Symbol | :between | Alignment (:left, :center, :right, :between) | | `icon_only` | Boolean | false | Show only icons on prev/next buttons | | `show_jumper` | Boolean | false | Show go-to-page input field | | `base_url` | String | nil | Base URL for links | | `page_param` | Symbol | :page | URL parameter name for page | | `turbo_frame` | String | nil | Turbo Frame ID for AJAX | | `turbo_action` | Symbol | :replace | Turbo action (:replace, :advance) | | `show_info` | Boolean | true | Show "Showing X-Y of Z" | | `show_per_page` | Boolean | false | Show per-page selector | | `per_page_options` | Array | [10,25,50,100] | Per-page dropdown options | | `items_per_page` | Integer | nil | Current items per page | | `window` | Integer | 2 | Pages around current | | `outer_window` | Integer | 1 | Pages at edges | | `prev_text` | String | "Previous" | Previous button text | | `next_text` | String | "Next" | Next button text | | `first_text` | String | "First" | First button text | | `last_text` | String | "Last" | Last button text | ## Accessibility - Uses semantic `
<% end %> <% steps.with_step("Profile") do %>
<% end %> <% steps.with_step("Confirm") do %>

Review and click Submit to complete.

<% end %> <% end %> ``` --- ## API Reference ### Component Parameters #### Required | Parameter | Type | Description | |-----------|------|-------------| | `id` | String | Unique identifier for the wizard (used for DOM ids, storage keys) | | `url` | String | Form submission URL for step navigation | #### Optional | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `current_step` | Integer | `0` | Current step index (0-based) | | `variant` | Symbol | `:horizontal` | Layout variant (see [Variants](#variants)) | | `navigation` | Symbol | `:linear` | Navigation mode (see [Navigation Modes](#navigation-modes)) | | `size` | Symbol | `:md` | Indicator size: `:sm`, `:md`, `:lg` | | `color` | Symbol | `:primary` | Theme color for active step | | `indicators_position` | Symbol | `:top` | Position: `:top`, `:bottom`, `:none` | | `buttons_position` | Symbol | `:bottom` | Position: `:top`, `:bottom`, `:none` | | `cache_locally` | Boolean | `true` | Cache current step in LocalStorage | | `client_validation` | Boolean | `true` | Run HTML5 validation before next | | `error_step` | Integer | `nil` | Step index with error (shows red) | | `back_text` | String | `"Back"` | Back button label | | `next_text` | String | `"Next"` | Next button label | | `submit_text` | String | `"Submit"` | Submit button label (last step) | | `loading_text` | String | `"Saving..."` | Loading state text | | `turbo_frame_id` | String | `nil` | Custom turbo frame ID | #### Parameter Values Reference | Parameter | Valid Values | |-----------|-------------| | `variant` | `:horizontal`, `:vertical`, `:minimal`, `:progress`, `:breadcrumb`, `:timeline`, `:vertical_cards` | | `navigation` | `:linear`, `:free`, `:completed_only` | | `size` | `:sm`, `:md`, `:lg` | | `color` | `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:blue`, `:purple`, `:pink`, `:amber`, `:cyan`, `:indigo`, `:teal`, `:orange`, `:rose`, `:violet`, `:emerald`, `:sky`, `:lime`, `:fuchsia` | | `indicators_position` | `:top`, `:bottom`, `:none` | | `buttons_position` | `:top`, `:bottom`, `:none` | --- ### Step Parameters Each step is defined using `steps.with_step`. The title can be passed as a positional argument or keyword. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `title` | String | **Yes** | - | Step title (positional or keyword) | | `description` | String | No | `nil` | Subtitle text (vertical/timeline variants) | | `icon` | Symbol | No | `nil` | Icon name instead of step number | | `optional` | Boolean | No | `false` | Mark step as skippable | **Positional title (cleaner):** ```erb <% steps.with_step("Account") do %> Content here <% end %> ``` **Keyword title (explicit):** ```erb <% steps.with_step(title: "Account") do %> Content here <% end %> ``` **With all options:** ```erb <% steps.with_step("Payment", description: "Enter card details", icon: :credit_card, optional: false) do %> Content here <% end %> ``` --- ### Layout Slots Two slots allow layout customization for additional content: | Slot | Method | Position | Description | |------|--------|----------|-------------| | **Header** | `with_header` | Above everything | Custom content at the top | | **Footer** | `with_footer` | Below everything | Custom content at the bottom | ```erb <%= rui_steps(id: "wizard", url: path) do |steps| %> <% steps.with_header do %> <% end %> <% steps.with_step("Step 1") { "content" } %> <% steps.with_footer do %> <% end %> <% end %> ``` **Position Controls** (for built-in elements): - `indicators_position: :top` (default), `:bottom`, `:none` - `buttons_position: :top`, `:bottom` (default), `:none` --- ## Step Title Syntax The Steps component supports two equivalent syntaxes for step titles. Both produce identical results. ### Positional Title (Recommended) Pass the title as the first argument. This is cleaner and more readable: ```erb <%# Simple step %> <% steps.with_step("Account") do %>

Step content here

<% end %> <%# With icon %> <% steps.with_step("Payment", icon: :credit_card) do %>

Payment form here

<% end %> <%# With description (vertical/timeline variants) %> <% steps.with_step("Shipping", description: "Enter delivery address") do %>

Address form here

<% end %> <%# With all options %> <% steps.with_step("Extras", description: "Optional add-ons", icon: :sparkles, optional: true) do %>

Optional content

<% end %> ``` ### Keyword Title (Explicit) Pass the title as a keyword argument. Useful when you prefer explicit naming: ```erb <% steps.with_step(title: "Account") do %>

Step content here

<% end %> <% steps.with_step(title: "Payment", icon: :credit_card) do %>

Payment form here

<% end %> ``` ### Mixing Syntaxes You can mix both syntaxes in the same wizard: ```erb <%= rui_steps(id: "wizard", url: path) do |steps| %> <% steps.with_step("Account") do %>

Positional title

<% end %> <% steps.with_step(title: "Profile", description: "About you") do %>

Keyword title with description

<% end %> <% steps.with_step("Confirm", icon: :check_circle) do %>

Positional title with icon

<% end %> <% end %> ``` ### Title is Required A step without a title raises `ArgumentError`: ```erb <%# This will raise an error! %> <% steps.with_step(icon: :user) do %>

Missing title

<% end %> ``` Error: `ArgumentError: Step requires a title` --- ## Layout Slots (Header/Footer) Layout slots let you add custom content above and below the wizard. The built-in elements (indicators and buttons) are controlled via position params. ### Visual Layout (Default: indicators_position: :top, buttons_position: :bottom) ``` +--------------------------------------------------+ | HEADER SLOT (with_header) | | Custom content (title, description, cancel) | +--------------------------------------------------+ | STEP INDICATORS (indicators_position: :top) | | [1] ─── [2] ─── [3] ─── [4] | +--------------------------------------------------+ | STEP CONTENT | | Current step's content area | +--------------------------------------------------+ | NAV BUTTONS (buttons_position: :bottom) | | [ Back ] [ Next ] | +--------------------------------------------------+ | FOOTER SLOT (with_footer) | | Custom content (help links, terms, progress) | +--------------------------------------------------+ ``` ### Position Params | Param | Options | Default | Description | |-------|---------|---------|-------------| | `indicators_position` | `:top`, `:bottom`, `:none` | `:top` | Where step indicators appear | | `buttons_position` | `:top`, `:bottom`, `:none` | `:bottom` | Where nav buttons appear | **Swapped Layout** (indicators_position: :bottom, buttons_position: :top): ``` +--------------------------------------------------+ | HEADER SLOT (with_header) | +--------------------------------------------------+ | NAV BUTTONS (buttons_position: :top) | | [ Back ] [ Next ] | +--------------------------------------------------+ | STEP CONTENT | | Current step's content area | +--------------------------------------------------+ | STEP INDICATORS (indicators_position: :bottom) | | [1] ─── [2] ─── [3] ─── [4] | +--------------------------------------------------+ | FOOTER SLOT (with_footer) | +--------------------------------------------------+ ``` --- ### Header Slot Add custom content above the step indicators. Great for: - Wizard title and description - Cancel/Close buttons - Progress summary - Breadcrumbs ```erb <%= rui_steps(id: "wizard", url: wizard_path) do |steps| %> <% steps.with_header do %>

Account Setup

Complete these steps to activate your account

<% end %> <% steps.with_step("Account") { "..." } %> <% steps.with_step("Profile") { "..." } %> <% steps.with_step("Done") { "..." } %> <% end %> ``` **Header with Cancel Button:** ```erb <% steps.with_header do %>

New Project Wizard

<%= rui_button("Cancel", variant: :ghost, size: :sm, href: projects_path) %>
<% end %> ``` --- ### Footer Slot Add custom content below everything (after navigation buttons). Great for: - Help links - Terms and conditions - Progress saved indicator - Additional context ```erb <%= rui_steps(id: "wizard", url: wizard_path) do |steps| %> <% steps.with_step("Account") { "..." } %> <% steps.with_step("Profile") { "..." } %> <% steps.with_footer do %>
<% end %> <% end %> ``` **Footer with Terms:** ```erb <% steps.with_footer do %>

By continuing, you agree to our Terms of Service and Privacy Policy.

<% end %> ``` --- ### Custom Navigation To add custom navigation, hide the default buttons and add your own in the header or footer slot. ```erb <%= rui_steps( id: "wizard", url: wizard_path, buttons_position: :none <%# Hide default buttons %> ) do |steps| %> <% steps.with_header do %>
Setup Progress
<%= rui_button("Back", size: :sm, variant: :outline, data: { action: "rapid-rails-ui--steps#back" }) %> <%= rui_button("Skip", size: :sm, variant: :ghost, data: { action: "rapid-rails-ui--steps#next" }) %> <%= rui_button("Next", size: :sm, data: { action: "rapid-rails-ui--steps#next" }) %>
<% end %> <% steps.with_step("Account") { "..." } %> <% steps.with_step("Profile") { "..." } %> <% end %> ``` #### Stimulus Actions for Custom Buttons Use these `data-action` values to trigger navigation: | Action | Stimulus Target | Description | |--------|-----------------|-------------| | `rapid-rails-ui--steps#back` | Back button | Go to previous step | | `rapid-rails-ui--steps#next` | Next button | Go to next step (validates first) | | `rapid-rails-ui--steps#goToStep` | Jump to step | Requires `data-step` attribute | ```erb <%# Go to specific step %> <%= rui_button("Go to Step 2", data: { action: "rapid-rails-ui--steps#goToStep", step: 1 }) %> ``` --- ### Combining Slots Use all three slots together for complete layout control: ```erb <%= rui_steps( id: "onboarding", url: onboarding_path, current_step: @current_step, color: :blue ) do |steps| %> <%# ========== HEADER ========== %> <% steps.with_header do %>

Welcome to Acme

Let's get you set up in just a few steps

<% end %> <%# ========== STEPS ========== %> <% steps.with_step("Account", icon: :user) do %>

Create your account

<% end %> <% steps.with_step("Profile", icon: :identification) do %>

Tell us about yourself

<% end %> <% steps.with_step("Preferences", icon: :cog_6_tooth) do %>

Customize your experience

<% end %> <% steps.with_step("Done", icon: :check_circle) do %>

You're all set!

Click "Get Started" to begin using Acme.

<% end %> <%# ========== FOOTER ========== %> <% steps.with_footer do %>
Your progress is saved automatically Need help?
<% end %> <% end %> ``` --- ### Real-World Slot Patterns #### Pattern 1: Header with Inline Navigation Put navigation in the header, hide default buttons: ```erb <%= rui_steps( id: "compact-wizard", url: wizard_path, buttons_position: :none <%# Hide default buttons %> ) do |steps| %> <% steps.with_header do %>
Setup Progress
<%= rui_button("Back", size: :sm, variant: :outline, data: { action: "rapid-rails-ui--steps#back" }) %> <%= rui_button("Next", size: :sm, data: { action: "rapid-rails-ui--steps#next" }) %>
<% end %> <% steps.with_step("Step 1") { "Content..." } %> <% steps.with_step("Step 2") { "Content..." } %> <% steps.with_step("Step 3") { "Content..." } %> <% end %> ``` #### Pattern 2: Wizard in a Modal Minimal variant with custom footer for modal context: ```erb <%= rui_dialog(id: "setup-modal", size: :lg) do |dialog| %> <% dialog.with_body do %> <%= rui_steps( id: "modal-wizard", url: setup_path, variant: :minimal, current_step: @step ) do |steps| %> <% steps.with_step("Connect") do %>

Connect your account to continue.

<%= rui_button("Connect with Google", variant: :outline, class: "w-full") %>
<% end %> <% steps.with_step("Import") do %>

Drag files here or click to browse

<% end %> <% steps.with_step("Done") do %>

Setup complete!

<% end %> <% steps.with_footer do %>

You can always change these settings later.

<% end %> <% end %> <% end %> <% end %> ``` #### Pattern 3: Checkout Flow with Custom Actions E-commerce checkout with contextual buttons: ```erb <%= rui_steps( id: "checkout", url: checkout_path, variant: :progress, color: :emerald ) do |steps| %> <% steps.with_header do %>

Checkout

Order #<%= @order.number %>
<% end %> <% steps.with_step("Cart", icon: :shopping_cart) do %> <%= render "checkout/cart_summary" %> <% end %> <% steps.with_step("Shipping", icon: :truck) do %> <%= render "checkout/shipping_form" %> <% end %> <% steps.with_step("Payment", icon: :credit_card) do %> <%= render "checkout/payment_form" %> <% end %> <% steps.with_step("Confirm", icon: :check) do %> <%= render "checkout/confirmation" %> <% end %> <% steps.with_footer do %>
Secure checkout | Need help?
<% end %> <% end %> ``` #### Pattern 4: Registration with Social Login in Footer ```erb <%= rui_steps(id: "register", url: register_path) do |steps| %> <% steps.with_step("Account") do %>
<% end %> <% steps.with_step("Verify") do %>

Enter the code sent to your email

<% end %> <% steps.with_step("Welcome") do %>

Welcome aboard!

Your account is ready.

<% end %> <% steps.with_footer do %>

Or continue with

<%= rui_button("Google", variant: :outline, size: :sm) %> <%= rui_button("GitHub", variant: :outline, size: :sm) %> <%= rui_button("Apple", variant: :outline, size: :sm) %>
<% end %> <% end %> ``` --- ## Variants ### Horizontal (Default) Inline layout with circles, titles, and connectors in a row. Best for 3-5 steps. ```erb <%= rui_steps( id: "horizontal-demo", url: wizard_path, variant: :horizontal, current_step: 1 ) do |steps| %> <% steps.with_step("Account") do %>

Create Your Account

Enter your email and password.

<% end %> <% steps.with_step("Profile") do %>

Set Up Your Profile

Tell us about yourself.

<% end %> <% steps.with_step("Settings") do %>

Configure Settings

Customize your experience.

<% end %> <% steps.with_step("Confirm") do %>

Review & Submit

Confirm your information.

<% end %> <% end %> ``` ### Vertical Sidebar navigation on left, content on right. Best for complex wizards with descriptions. ```erb <%= rui_steps( id: "vertical-demo", url: wizard_path, variant: :vertical, current_step: 0 ) do |steps| %> <% steps.with_step("Personal Info", description: "Your basic details") do %>

Personal Information

<% end %> <% steps.with_step("Contact", description: "How to reach you") do %>

Contact Information

<% end %> <% steps.with_step("Address", description: "Where you live") do %>

Address

<% end %> <% steps.with_step("Review", description: "Confirm details") do %>

Review Your Information

<% end %> <% end %> ``` ### Minimal Simple "Step X of Y: Title" header. Best for dialogs and modals. ```erb <%= rui_steps( id: "minimal-demo", url: wizard_path, variant: :minimal, current_step: 0 ) do |steps| %> <% steps.with_step("Connect Account") do %>

Connect your account to get started.

<% end %> <% steps.with_step("Import Data") do %>

Drag and drop files here

<% end %> <% steps.with_step("Finish") do %>

All Done!

<% end %> <% end %> ``` ### Progress Icon-only circles with thick connectors. Compact design for headers. ```erb <%= rui_steps( id: "progress-demo", url: checkout_path, variant: :progress, current_step: 1 ) do |steps| %> <% steps.with_step("Cart", icon: :shopping_cart) do %>

Your Cart

<% end %> <% steps.with_step("Shipping", icon: :truck) do %>

Shipping Address

<% end %> <% steps.with_step("Payment", icon: :credit_card) do %>

Payment Method

<% end %> <% steps.with_step("Confirm", icon: :check) do %>

Order Confirmed!

<% end %> <% end %> ``` ### Breadcrumb Compact chevron-separated badges. Best for navigation-style progress. ```erb <%= rui_steps( id: "breadcrumb-demo", url: onboarding_path, variant: :breadcrumb, navigation: :free, current_step: 1 ) do |steps| %> <% steps.with_step("Account") { "Account setup content" } %> <% steps.with_step("Preferences") { "Preferences content" } %> <% steps.with_step("Theme") { "Theme selection" } %> <% steps.with_step("Complete") { "Setup complete!" } %> <% end %> ``` ### Timeline Vertical timeline with left border spine. Best for process flows. ```erb <%= rui_steps( id: "timeline-demo", url: application_path, variant: :timeline, current_step: 2 ) do |steps| %> <% steps.with_step("Application Submitted", description: "Jan 15, 2024") do %>

Your application has been received.

<% end %> <% steps.with_step("Documents Reviewed", description: "Jan 18, 2024") do %>

All required documents verified.

<% end %> <% steps.with_step("Interview Scheduled", description: "Jan 22, 2024") do %>

Your interview is scheduled.

<% end %> <% steps.with_step("Decision Pending", description: "Estimated: Jan 30") do %>

Awaiting final decision.

<% end %> <% end %> ``` ### Vertical Cards Alert-style bordered cards stacked vertically. Best for clickable step selection. ```erb <%= rui_steps( id: "cards-demo", url: setup_path, variant: :vertical_cards, navigation: :free, current_step: 0, color: :purple ) do |steps| %> <% steps.with_step("Connect Account", description: "Link your existing account", icon: :link) do %>

Connect Account

<% end %> <% steps.with_step("Import Data", description: "Bring in your content", icon: :arrow_down_tray) do %>

Drag and drop files here

<% end %> <% steps.with_step("All Done!", description: "You're ready to go") do %>

Setup Complete!

<% end %> <% end %> ``` --- ## Sizes Control the size of step indicators. | Size | Circle | Description | |------|--------|-------------| | `:sm` | 32px (w-8) | Compact, small text | | `:md` | 40px (w-10) | Default size | | `:lg` | 48px (w-12) | Large, prominent | ```erb <%# Small %> <%= rui_steps(id: "sm", url: path, size: :sm) do |steps| %> <% steps.with_step("Step 1") { "Small indicators" } %> <% steps.with_step("Step 2") { "Compact design" } %> <% end %> <%# Medium (default) %> <%= rui_steps(id: "md", url: path, size: :md) do |steps| %> <% steps.with_step("Step 1") { "Default size" } %> <% steps.with_step("Step 2") { "Balanced appearance" } %> <% end %> <%# Large %> <%= rui_steps(id: "lg", url: path, size: :lg) do |steps| %> <% steps.with_step("Step 1") { "Large indicators" } %> <% steps.with_step("Step 2") { "Maximum visibility" } %> <% end %> ``` --- ## Colors Customize the theme color for active steps. Completed steps always use green. Error steps always use red. | Category | Colors | |----------|--------| | Semantic | `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info` | | Tailwind | `:blue`, `:green`, `:red`, `:purple`, `:pink`, `:amber`, `:cyan`, `:indigo`, `:teal`, `:orange`, `:rose`, `:violet`, `:emerald`, `:sky`, `:lime`, `:fuchsia` | ```erb <%# Blue theme %> <%= rui_steps(id: "blue", url: path, color: :blue) do |steps| %> <% steps.with_step("Account") { "Blue theme" } %> <% steps.with_step("Profile") { "Active step is blue" } %> <% end %> <%# Purple theme %> <%= rui_steps(id: "purple", url: path, color: :purple) do |steps| %> <% steps.with_step("Start") { "Purple theme" } %> <% steps.with_step("Finish") { "Elegant purple" } %> <% end %> <%# Danger theme (for destructive flows) %> <%= rui_steps(id: "danger", url: path, color: :danger) do |steps| %> <% steps.with_step("Warning") { "Danger/red theme" } %> <% steps.with_step("Confirm") { "Deletion wizard" } %> <% end %> ``` --- ## Navigation Modes | Mode | Description | |------|-------------| | `:linear` | Only Back/Next buttons work. Indicators not clickable. | | `:free` | All step indicators are clickable. Jump to any step. | | `:completed_only` | Can click completed steps and current+1. Cannot skip ahead. | ```erb <%# Linear (default) - must use buttons %> <%= rui_steps(id: "linear", url: path, navigation: :linear) do |steps| %> <% steps.with_step("Step 1") { "Must complete in order" } %> <% steps.with_step("Step 2") { "Use Back/Next only" } %> <% end %> <%# Free - click any step %> <%= rui_steps(id: "free", url: path, navigation: :free) do |steps| %> <% steps.with_step("Step 1") { "Click any step to jump" } %> <% steps.with_step("Step 2") { "Full freedom" } %> <% end %> <%# Completed only - can go back, not skip ahead %> <%= rui_steps(id: "completed", url: path, navigation: :completed_only) do |steps| %> <% steps.with_step("Step 1") { "Can revisit completed steps" } %> <% steps.with_step("Step 2") { "Cannot skip ahead" } %> <% end %> ``` --- ## Position Options Control placement of indicators and buttons (horizontal variant). ```erb <%# Default: indicators top, buttons bottom %> <%= rui_steps(id: "default", url: path, indicators_position: :top, buttons_position: :bottom) do |steps| %> <% steps.with_step("Step 1") { "Default layout" } %> <% end %> <%# Flipped: buttons top, indicators bottom %> <%= rui_steps(id: "flipped", url: path, indicators_position: :bottom, buttons_position: :top) do |steps| %> <% steps.with_step("Step 1") { "Flipped layout" } %> <% end %> <%# Hide indicators %> <%= rui_steps(id: "no-ind", url: path, indicators_position: :none) do |steps| %> <% steps.with_step("Step 1") { "No indicators" } %> <% end %> <%# Hide buttons (use with navigation: :free) %> <%= rui_steps(id: "no-btn", url: path, buttons_position: :none, navigation: :free) do |steps| %> <% steps.with_step("Step 1") { "Click indicators to navigate" } %> <% end %> <%# Hide both (provide your own UI) %> <%= rui_steps(id: "none", url: path, indicators_position: :none, buttons_position: :none) do |steps| %> <% steps.with_step("Step 1") do %>

Provide your own navigation

<% end %> <% end %> ``` --- ## Icons Use icons instead of step numbers. Common icons: ```erb <%# Account/User steps %> <% steps.with_step("Account", icon: :user) { "..." } %> <% steps.with_step("Profile", icon: :user_circle) { "..." } %> <% steps.with_step("Identity", icon: :identification) { "..." } %> <%# Shopping/Checkout %> <% steps.with_step("Cart", icon: :shopping_cart) { "..." } %> <% steps.with_step("Payment", icon: :credit_card) { "..." } %> <% steps.with_step("Shipping", icon: :truck) { "..." } %> <%# Settings %> <% steps.with_step("Settings", icon: :cog_6_tooth) { "..." } %> <% steps.with_step("Preferences", icon: :adjustments_horizontal) { "..." } %> <%# Documents %> <% steps.with_step("Upload", icon: :arrow_up_tray) { "..." } %> <% steps.with_step("Download", icon: :arrow_down_tray) { "..." } %> <% steps.with_step("Review", icon: :document_text) { "..." } %> <%# Security %> <% steps.with_step("Security", icon: :shield_check) { "..." } %> <% steps.with_step("Lock", icon: :lock_closed) { "..." } %> <%# Confirmation %> <% steps.with_step("Confirm", icon: :check) { "..." } %> <% steps.with_step("Complete", icon: :check_circle) { "..." } %> ``` --- ## Custom Button Text ```erb <%= rui_steps( id: "custom", url: path, back_text: "Previous", next_text: "Continue", submit_text: "Finish Setup", loading_text: "Processing..." ) do |steps| %> <% steps.with_step("Step 1") { "Back shows 'Previous'" } %> <% steps.with_step("Step 2") { "Next shows 'Continue'" } %> <% steps.with_step("Step 3") { "Last step shows 'Finish Setup'" } %> <% end %> ``` ### Localization ```erb <%= rui_steps( id: "i18n", url: path, back_text: t("wizard.back"), next_text: t("wizard.next"), submit_text: t("wizard.submit"), loading_text: t("wizard.loading") ) do |steps| %> <% steps.with_step(t("wizard.step1")) { "..." } %> <% steps.with_step(t("wizard.step2")) { "..." } %> <% end %> ``` --- ## Error Handling ```erb <%= rui_steps( id: "errors", url: path, current_step: @current_step, error_step: @error_step <%# Pass step index with error %> ) do |steps| %> <% steps.with_step("Account") do %> <% if @error_step == 0 %>

Please fix the errors below.

<% end %>
<% if @user.errors[:email].any? %>

<%= @user.errors[:email].first %>

<% end %>
<% end %> <% end %> ``` ### Controller Pattern ```ruby def update case params[:direction] when "next" if @wizard.valid_for_step?(@wizard.current_step) @wizard.increment!(:current_step) @error_step = nil else @error_step = @wizard.current_step end when "back" @wizard.decrement!(:current_step) @error_step = nil end render :show end ``` --- ## Caching & Validation ### LocalStorage Caching ```erb <%# Enable (default) %> <%= rui_steps(id: "cached", url: path, cache_locally: true) do |steps| %> <% steps.with_step("Step 1") { "Progress saved in LocalStorage" } %> <% end %> <%# Disable %> <%= rui_steps(id: "no-cache", url: path, cache_locally: false) do |steps| %> <% steps.with_step("Step 1") { "Always uses server state" } %> <% end %> ``` Storage key: `rui_steps_{id}_current` ### Client Validation ```erb <%# Enable (default) - HTML5 validation before next %> <%= rui_steps(id: "validated", url: path, client_validation: true) do |steps| %> <% steps.with_step("Step 1") do %> <%# Must be valid to proceed %> <% end %> <% end %> <%# Disable %> <%= rui_steps(id: "no-val", url: path, client_validation: false) do |steps| %> <% steps.with_step("Step 1") do %> <%# Can proceed without validation %> <% end %> <% end %> ``` --- ## Controller Setup ### Basic Controller ```ruby class WizardsController < ApplicationController before_action :set_wizard def show @current_step = @wizard.current_step end def update case params[:direction] when "next" handle_next_step when "back" @wizard.decrement!(:current_step) if @wizard.current_step > 0 when /^goto:(\d+)$/ handle_goto_step($1.to_i) end respond_to do |format| format.turbo_stream format.html { render :show } end end private def set_wizard @wizard = Wizard.find_or_create_by(session_id: session.id) @current_step = @wizard.current_step end def handle_next_step if @wizard.valid_for_step?(@wizard.current_step) if @wizard.current_step == @wizard.total_steps - 1 @wizard.complete! redirect_to wizard_complete_path else @wizard.increment!(:current_step) end else @error_step = @wizard.current_step end end def handle_goto_step(target) @wizard.update!(current_step: target) if can_navigate_to?(target) end def can_navigate_to?(step) case @wizard.navigation_mode when "free" then true when "completed_only" then step <= @wizard.current_step + 1 else false end end end ``` ### Turbo Stream Response ```erb <%# app/views/wizards/update.turbo_stream.erb %> <%= turbo_stream.replace "wizard-step-content" do %> <%= render partial: "step_#{@current_step}" %> <% end %> ``` --- ## Stimulus Controller ### Values | Value | Type | Description | |-------|------|-------------| | `current` | Number | Current step index | | `total` | Number | Total number of steps | | `navigation` | String | Navigation mode | | `cacheLocally` | Boolean | Cache in LocalStorage | | `storageKey` | String | LocalStorage key | | `clientValidation` | Boolean | Validate before transitions | | `errorStep` | Number | Step with error (-1 if none) | ### Actions | Action | Description | |--------|-------------| | `next` | Go to next step (validates first) | | `back` | Go to previous step | | `goToStep` | Jump to specific step | | `handleKeydown` | Keyboard navigation | ### Public Methods ```javascript // Get controller reference const controller = this.application.getControllerForElementAndIdentifier( document.getElementById("wizard-id"), "rapid-rails-ui--steps" ); // Error management controller.setError(2); // Mark step 2 as error controller.clearError(2); // Clear error from step 2 controller.clearAllErrors(); // Clear all errors // Storage controller.clearStorage(); // Clear LocalStorage cache ``` ### Events | Event | Description | |-------|-------------| | `before-navigate` | Before step transition | | `after-navigate` | After step transition | | `step-changed` | When step changes | | `validation-failed` | When validation fails | | `error-set` | When error state is set | | `error-cleared` | When error is cleared | ```javascript // Listen for events element.addEventListener("step-changed", (event) => { console.log("Step changed to:", event.detail.step); }); ``` --- ## Accessibility ### ARIA Attributes - `