Common Patterns
Real-world examples of using RapidRails UI components in typical Rails applications.
Form Patterns
RapidRails UI integrates deeply with Rails FormBuilder, providing semantic form handling and smart defaults. Forms are accessible, responsive, and work seamlessly with Turbo and form validation. Buttons intelligently adapt their labels based on model state (new vs. edit).
Basic Form with Auto-Labeled Button
The form button automatically shows "Create Post" for new records or "Update Post" for persisted records:
<%= form_with model: @post, class: "space-y-6" do |f| %>
<%= f.rui_input(:title,
label: "Title",
placeholder: "Enter post title",
required: true,
size: :lg,
color: :primary) %>
<%= f.rui_textarea(:excerpt,
label: "Excerpt",
placeholder: "Brief summary for previews...",
rows: 3,
help_text: "Shows in post cards and search results") %>
<div class="flex gap-3">
<%= f.rui_button(color: :primary) %>
<%= link_to "Cancel", posts_path, class: "..." %>
</div>
<% end %>
Form with Multiple Actions
Combine save and draft actions in a single form, each with distinct styling and intent:
<%= form_with model: @post, class: "space-y-6" do |f| %>
<%# ... form fields ... %>
<div class="flex gap-3">
<%# Primary action: Save with icon %>
<%= f.rui_button(color: :primary, size: :lg) do |button|
button.with_icon(:save, position: :leading)
end %>
<%# Secondary action: Save as draft %>
<%= f.rui_button("Save Draft",
name: "draft",
variant: :outline,
color: :zinc,
size: :lg) %>
<%# Tertiary action: Cancel %>
<%= rui_link posts_path,
variant: :ghost,
color: :zinc,
size: :lg do |link|
link.with_icon(:x, position: :leading)
concat " Cancel"
end %>
</div>
<% end %>
Form with Validation & Error Handling
Use rui_errors for automatic, styled error display. Errors appear at the form top and next to individual fields:
<%= form_with model: @post, data: { turbo_submits_with: "Saving..." } do |f| %>
<%# Smart error component - auto-styles and displays %>
<%= rui_errors(@post) %>
<%= f.rui_input(:title,
label: "Title",
required: true,
color: :primary) %>
<div class="flex gap-3">
<%= f.rui_button(color: :primary,
data: { turbo_submits_with: "Saving..." }) %>
<%= rui_link posts_path, variant: :ghost do |link|
link.with_icon(:x)
end %>
</div>
<% end %>
Form Fields
RapidRails UI form fields include automatic labels, help text, placeholder support, sizing, color theming, and icon prefixes/suffixes. They're fully responsive and work with all input types.
Text Input (rui_input)
From simple text to emails, URLs, and numbers. Each input supports labels, help text, placeholders, icons, sizing, and semantic color variants:
<!-- Text input with label and help text -->
<%= f.rui_input(:title, label: "Post Title", help_text: "Appears in search results") %>
<!-- Large text input for prominent fields -->
<%= f.rui_input(:name, label: "Full Name", size: :lg, color: :primary) %>
<!-- Email field with icon prefix -->
<%= f.rui_input(:email, type: :email, label: "Email Address") do |input|
input.with_prefix { rui_icon :mail, size: :sm }
end %>
<!-- URL field with required indicator -->
<%= f.rui_input(:website, type: :url, label: "Website", required: true) %>
<!-- Number field with suffix -->
<%= f.rui_input(:price, type: :number, label: "Price") do |input|
input.with_suffix { span "$" }
end %>
Textarea (rui_textarea)
Rich multi-line text fields with character count, auto-expanding rows, and validation hints:
<!-- Basic textarea -->
<%= f.rui_textarea(:excerpt, label: "Excerpt", rows: 3) %>
<!-- With character count -->
<%= f.rui_textarea(:excerpt,
label: "Post Excerpt",
rows: 3,
maxlength: 300,
show_count: true,
help_text: "Brief summary shown in post cards (max 300 characters)") %>
<!-- Large textarea for content -->
<%= f.rui_textarea(:description,
label: "Full Description",
rows: 8,
placeholder: "Enter detailed description...",
color: :primary) %>
Date & Time Inputs (rui_date)
Smart date and time pickers with native date input support, optional date ranges, and semantic color coding:
<!-- Simple date picker -->
<%= f.rui_date(:published_at, label: "Publish Date") %>
<!-- Date with picker variant -->
<%= f.rui_date(:published_at,
variant: :picker,
label: "Publish Date",
help_text: "When this post goes live",
color: :primary) %>
<!-- Date with minimum/maximum constraints -->
<%= f.rui_date(:deadline,
label: "Deadline",
min: Date.today,
help_text: "For time-sensitive content",
color: :danger) %>
<!-- Time input -->
<%= f.rui_date(:schedule_time,
type: :time,
label: "Schedule Time",
help_text: "Specific time to publish") %>
<!-- Date range (start and end dates) -->
<%= f.rui_date(:visibility_period,
variant: :picker,
range: true,
label: "Visibility Window",
help_text: "Auto-show/hide dates") %>
Form Controls
Checkboxes and radio buttons with multiple layout variants, descriptions, and icon support for sophisticated form experiences.
Checkboxes (rui_checkbox)
From simple toggles to switch-style controls with descriptions and semantic colors:
<!-- Simple checkbox -->
<%= f.rui_checkbox(:featured, label: "Featured Post") %>
<!-- Checkbox with description -->
<%= f.rui_checkbox(:featured,
label: "Featured Post",
description: "Show prominently on homepage",
color: :warning) %>
<!-- Switch-style checkbox -->
<%= f.rui_checkbox(:notify_subscribers,
label: "Notify Subscribers",
description: "Email notification on publish",
variant: :switch,
color: :success) %>
<!-- Multiple checkboxes in one group -->
<%= f.rui_checkbox(:allow_comments,
label: "Allow Comments",
description: "Enable reader comments",
color: :info) %>
Radio Buttons (rui_radio_button)
Radio buttons with pill, card, and standard layouts. Support descriptions and semantic color coding:
<!-- Horizontal pill layout (compact, space-efficient) -->
<%= f.rui_radio_button(:category,
collection: [
["Technology", "Technology"],
["Design", "Design"],
["Product", "Product"],
["Engineering", "Engineering"]
],
variant: :pill,
layout: :horizontal,
label: "Category",
color: :primary) %>
<!-- Card layout with descriptions (expressive, feature-rich) -->
<%= f.rui_radio_button(:status,
collection: [
["Draft", "draft", "Save without publishing"],
["Published", "published", "Make visible to readers"],
["Archived", "archived", "Hide from public view"]
],
variant: :card,
description_method: 2,
label: "Status",
color: :primary) %>
<!-- Standard vertical layout (classic look) -->
<%= f.rui_radio_button(:visibility,
collection: ["Public", "Unlisted", "Private"],
label: "Visibility",
color: :zinc) %>
Typography (rui_text)
Semantic text component supporting heading levels, sizes, weights, and color variants. Ensures consistent typography across your application:
<!-- Heading (h1-h6) with size variant -->
<%= rui_text "Create New Post", as: :h1, size: :xl5, weight: :bold %>
<!-- Section heading with semibold weight -->
<%= rui_text "Publishing Options", as: :h3, size: :lg, weight: :semibold %>
<!-- Colored text -->
<%= rui_text "Featured Post", color: :warning, weight: :semibold %>
<!-- Custom classes (merges with component styling) -->
<%= rui_text @post.title,
as: :h1,
size: :xl5,
weight: :bold,
class: "text-zinc-900 dark:text-zinc-100 mb-6" %>
Display Components
Visual components for displaying images, badges, avatars, and other content with built-in optimization and responsiveness.
Images (rui_image)
Optimized image display with lazy loading, responsive sizing, and object-fit support:
<!-- Hero image with eager loading -->
<%= rui_image @post.banner_image,
alt: @post.title,
size: :full,
fit: :cover,
loading: :eager,
fetchpriority: :high,
class: "w-full h-full object-cover" %>
<!-- Card image with lazy loading -->
<%= rui_image post.banner_image,
alt: post.title,
size: :full,
fit: :cover,
zoom: true,
class: "w-full h-full object-cover transition-transform group-hover:scale-105" %>
Badges (rui_badge)
Status indicators and labels with semantic color coding and optional icons:
<!-- Simple badge -->
<%= rui_badge "Featured", color: :warning, size: :sm %>
<!-- Category badge -->
<%= rui_badge post.category, color: :primary, size: :sm %>
<!-- Status badge with semantic color -->
<%= rui_badge @post.status.capitalize,
color: @post.status == "published" ? :success : :zinc,
size: :sm %>
<!-- Badge with icon -->
<%= rui_badge "Subscribers notified", color: :success do |badge|
badge.with_icon(:bell, size: :xs)
end %>
Avatars (rui_avatar)
User avatars with initials fallback and optional ring indicators:
<!-- Avatar with image and ring -->
<%= rui_avatar src: @post.author_avatar,
name: @post.author_name,
size: :md,
ring: :white %>
<!-- Avatar with just name (shows initials) -->
<%= rui_avatar name: "Jane Smith", size: :lg %>
<!-- Featured avatar with primary ring -->
<%= rui_avatar src: post.author_avatar,
name: post.author_name || "Author",
size: :lg,
ring: :primary %>
Clipboard (rui_clipboard)
Copy-to-clipboard buttons with visual feedback:
<!-- Copy URL to clipboard -->
<span id="post-url" class="sr-only"><%= post_url(@post) %></span>
<%= rui_clipboard "post-url", variant: :github, tooltip: "Copy link", size: :sm %>
User Feedback
Components for displaying errors, flash messages, and other user feedback with semantic styling and accessibility support.
Error Display (rui_errors)
Automatically styled form error display with full message list:
<!-- At top of form -->
<%= form_with model: @post do |f| %>
<!-- Auto-displays all validation errors -->
<%= rui_errors(@post) %>
<%= f.rui_input(:title, ...) %>
...
<% end %>
Flash Messages
Toast-style notifications for success, error, and info messages. One of the biggest pain points in vanilla Rails is flash message handling, lots of boilerplate, styling, dismissing, animations. RUI handles all of it automatically.
Plain Rails (What You'd Need to Write)
<!-- In app/views/layouts/application.html.erb -->
<div id="flash-container" class="fixed top-4 right-4 z-50">
<% if notice.present? %>
<div id="notice" class="bg-green-500 text-white px-4 py-3 rounded-lg shadow-lg mb-2 flex items-center justify-between">
<span><%= notice %></span>
<button onclick="this.parentElement.remove()" class="ml-4 text-white hover:text-gray-200">
×
</button>
</div>
<script>
setTimeout(() => {
const el = document.getElementById('notice');
if (el) el.remove();
}, 5000);
</script>
<% end %>
<% if alert.present? %>
<div id="alert" class="bg-red-500 text-white px-4 py-3 rounded-lg shadow-lg mb-2 flex items-center justify-between">
<span><%= alert %></span>
<button onclick="this.parentElement.remove()" class="ml-4 text-white hover:text-gray-200">
×
</button>
</div>
<script>
setTimeout(() => {
const el = document.getElementById('alert');
if (el) el.remove();
}, 5000);
</script>
<% end %>
</div>
<!-- Styles in CSS -->
@media (prefers-color-scheme: dark) {
#notice { @apply bg-green-700; }
#alert { @apply bg-red-700; }
}
~50 lines + manual styling + JavaScript
RUI Components (What You Actually Write)
<!-- CHOOSE ONE OPTION: -->
<!-- OPTION 1: Fixed positioning (recommended) -->
<!-- In app/views/layouts/application.html.erb -->
<%= rui_flash_container(position: :top_right) %>
<!-- OPTION 2: Inline display -->
<!-- In app/views/layouts/application.html.erb or any view -->
<%= rui_flash %>
<!-- In controller: Just use normal Rails flash -->
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post created!"
else
render :new, alert: "Error"
end
end
<!-- That's it! -->
1 line in layout
That's **25x less code**, and the RUI version automatically includes:
- Automatic dismissal — Messages disappear after 5 seconds (configurable)
- Dismiss button — Users can close immediately if they want
- Smooth animations — Fade in/out transitions automatically
- Semantic colors — Notice = green, alert = red, automatically styled
- Dark mode — Colors adapt to theme automatically
- Positioning — Choose top-left, top-right, bottom-left, bottom-right
- Accessibility — ARIA alerts so screen readers announce messages
- No JavaScript bloat — Stimulus handles animations, no inline scripts
- Turbo compatible — Works seamlessly with Turbo Stream responses
<!-- Layout setup (one line, choose your approach) -->
<!-- For fixed positioning (recommended): -->
<%= rui_flash_container(position: :top_right) %>
<!-- OR for inline display: -->
<%= rui_flash %>
<!-- Controller usage (standard Rails) -->
redirect_to @post, notice: "Post published successfully!"
redirect_to posts_path, alert: "Something went wrong"
<!-- Turbo Stream usage (works automatically with either approach) -->
respond_to do |format|
format.turbo_stream do
flash.now.notice = "Post updated!"
render turbo_stream: turbo_stream.update("post_#{@post.id}", partial: "post", locals: { post: @post })
end
end
Two Approaches to Flash Messages:
📌 Option 1: Fixed Positioning (Recommended)
Use rui_flash_container(position: :top_right) to display flash messages in a fixed position on the screen. Messages stay in the same corner regardless of page scroll.
Positions: :top_left, :top_right, :bottom_left, :bottom_right
📍 Option 2: Inline Display
Use rui_flash to display flash messages inline at the location where you place the component. Useful for displaying feedback near specific content or within a specific section.
Place this component anywhere in your view where you want flash messages to appear.
Important: Use only ONE of these components. Choose the approach that best fits your application's UX design.
Why This Matters:
Flash messages are one of the most **frustrating aspects** of building Rails UIs manually. You end up with:
- Duplicate dismiss buttons and timeout logic across projects
- Manual styling that doesn't match your app's design
- Vanilla JavaScript in your layout (mixing concerns)
- Different behavior in different applications
- Z-index conflicts and positioning headaches
- No dark mode support until you manually add it
RUI handles all of this with **one line of code**. Once, in your layout, and you're done forever.
Complete Form Example: Blog Post
Here's a real-world blog post form showcasing all the form components working together. This demonstrates the sophistication and developer experience of RapidRails UI forms.
Full Form with All Features
<%= form_with(model: post, local: true, class: "space-y-8") do |f| %>
<%# SMART ERROR DISPLAY %>
<%= rui_errors(post) %>
<div class="space-y-6">
<%# MAIN CONTENT SECTION %>
<%= f.rui_input(:title, label: "Title", placeholder: "Enter your post title", required: true, size: :lg, color: :primary) %>
<%= f.rui_textarea(:excerpt, label: "Excerpt", rows: 3, maxlength: 300, show_count: true, help_text: "Brief summary") %>
<%# Rich text editor %>
<div class="space-y-2">
<%= f.label :content, "Content", class: "block text-sm font-medium" %>
<%= f.rich_text_area :content, class: "min-h-[300px]" %>
</div>
</div>
<%# MEDIA & METADATA SECTION %>
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<%= rui_text "Media & Metadata", as: :h3, size: :lg, weight: :semibold, class: "mb-6" %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%= f.rui_input(:banner_image, type: :url, label: "Banner Image URL") do |input|
input.with_prefix { rui_icon :image, size: :sm }
end %>
<%= f.rui_radio_button(:category, collection: [["Technology", "Technology"], ["Design", "Design"]], variant: :pill, label: "Category", color: :primary) %>
</div>
</div>
<%# AUTHOR INFORMATION SECTION %>
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<%= rui_text "Author Information", as: :h3, size: :lg, weight: :semibold, class: "mb-6" %>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<%= f.rui_input(:author_name, label: "Author Name") %>
<%= f.rui_input(:author_avatar, type: :url, label: "Avatar URL") %>
<%= f.rui_input(:reading_time, type: :number, label: "Reading Time") do |input|
input.with_suffix { span "min" }
end %>
</div>
</div>
<%# PUBLISHING OPTIONS SECTION %>
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<%= rui_text "Publishing Options", as: :h3, size: :lg, weight: :semibold, class: "mb-6" %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%= f.rui_radio_button(:status, collection: [["Draft", "draft", "Save only"], ["Published", "published", "Live"]], variant: :card, description_method: 2, label: "Status", color: :primary) %>
<%= f.rui_date(:published_at, variant: :picker, label: "Publish Date", color: :primary) %>
</div>
<%# CHECKBOXES WITH SWITCHES %>
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-4">
<%= f.rui_checkbox(:featured, label: "Featured Post", description: "Show on homepage", color: :warning) %>
<%= f.rui_checkbox(:notify_subscribers, label: "Notify Subscribers", variant: :switch, color: :success) %>
<%= f.rui_checkbox(:allow_comments, label: "Allow Comments", color: :info) %>
</div>
</div>
<%# ADVANCED SCHEDULING %>
<details class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<summary class="cursor-pointer flex items-center gap-2 font-medium">
<%= rui_icon :calendar, size: :sm %> Advanced Scheduling
</summary>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<%= f.rui_date(:event_date, label: "Event Date") %>
<%= f.rui_date(:deadline, label: "Deadline", color: :danger) %>
<%= f.rui_date(:schedule_time, type: :time, label: "Schedule Time") %>
<%= f.rui_date(:visibility_period, variant: :picker, range: true, label: "Visibility") %>
</div>
</details>
<%# FORM ACTIONS %>
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8 flex items-center justify-between">
<div class="flex items-center gap-3">
<%= f.rui_button(color: :primary, size: :lg) %>
<% if post.persisted? && post.status != "published" %>
<%= rui_link publish_post_path(post), turbo_method: :patch, color: :success do |link|
link.with_icon(:send, size: :sm)
end %>
<% end %>
</div>
<div class="flex items-center gap-3">
<% if post.persisted? %>
<%= rui_button_to post_path(post), method: :delete, variant: :ghost, color: :danger do |btn|
btn.with_icon(:trash_2, size: :sm)
end %>
<% end %>
<%= rui_link posts_path, variant: :ghost, color: :zinc do |link|
link.with_icon(:x, size: :sm)
end %>
</div>
</div>
<% end %>
What Makes This Form Sophisticated:
- ✅ Explicit Labels: Each field has a clear, semantic label that describes its purpose
- ✅ Validation Display: Errors shown automatically with styling via rui_errors
- ✅ Responsive Grid: Form sections adapt from 1 column on mobile to 2-4 on desktop
- ✅ Semantic Coloring: Fields use color variants (primary, danger, success, warning, info)
- ✅ Multiple Input Types: Text, textarea, date picker, time, URL, number, rich text editor
- ✅ Icon Integration: Input fields show icons as visual hints
- ✅ Radio Variants: Pill layout for compact categories, card layout for complex status selection with descriptions
- ✅ Checkbox Variants: Standard checkboxes and switch-style toggles with descriptions
- ✅ Advanced Scheduling: Collapsible section for less-common but powerful date options
- ✅ Dynamic Buttons: Primary action, secondary publish action, delete for persisted records, cancel link
- ✅ Turbo Integration: Works seamlessly with form submissions, validation errors, and Turbo Streams
- ✅ Dark Mode: All components automatically support light and dark themes
This single form demonstrates the power and sophistication of RapidRails UI components—compare the 200 lines above to what you'd need to build with vanilla HTML and CSS, and you'll see why developers love this framework.
Real-World Implementation: Production Blog Form
Below is the actual blog post form from the RapidRails UI platform. This isn't a simplified example—it's production-ready code that powers our documentation website. It demonstrates how RUI components create sophisticated, professional interfaces with minimal effort and boilerplate.
Click above to see this form live and interactive in your browser. All the components below are fully functional—try typing, selecting, and interacting with the form to experience the responsive validation and user experience.
Form Structure Overview
The form is organized into 6 logical sections, each separated by a visual divider. This approach helps users understand the form flow and locate fields intuitively:
1. Main Content
Title, excerpt, and rich text editor for the post body
2. Media & Metadata
Banner image URL and post category selection
3. Author Information
Author name, avatar, and reading time estimate
4. Publishing Options
Status, publish date, featured flag, and engagement settings
5. Advanced Scheduling
Optional: Event date, deadline, schedule time, and visibility window
6. Form Actions
Save, publish, delete, and cancel buttons with appropriate intents
1. Main Content Section
The opening fields that every post needs. The title uses a large, prominent style to emphasize its importance, while the excerpt includes a character counter to help users stay within the 300-character limit.
<div class="space-y-6">
<%# Title %>
<%= f.rui_input(:title,
label: "Title",
placeholder: "Enter your post title",
required: true,
size: :lg,
color: :primary
) %>
<%# Excerpt %>
<%= f.rui_textarea(:excerpt,
label: "Excerpt",
placeholder: "A brief summary of your post for previews and SEO...",
rows: 3,
maxlength: 300,
show_count: true,
help_text: "Brief summary shown in post cards (max 300 characters)"
) %>
<%# Content - Rich Text Editor %>
<div class="space-y-2">
<%= f.label :content, "Content", class: "block text-sm font-medium text-zinc-700 dark:text-zinc-300" %>
<%= f.rich_text_area :content,
class: "min-h-[300px] w-full rounded-lg border border-zinc-300 dark:border-zinc-700",
placeholder: "Write your post content here... (supports Markdown shortcuts)" %>
</div>
</div>
Why RUI Components Win Here:
- Built-in validation with the required indicator automatically styled
- Character counter (`show_count: true`) is built-in, not a custom JavaScript enhancement
- Help text provides context without cluttering the form
- Semantic sizing (`size: :lg`) makes important fields visually prominent using a standard system
- Dark mode automatic—no additional CSS needed
2. Media & Metadata Section
Visual divider (border-top) signals a new section. The category uses pill-variant radio buttons for a modern, compact interface instead of a dropdown menu.
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<%= rui_text "Media & Metadata", as: :h3, size: :lg, weight: :semibold, class: "mb-6" %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%# Banner Image URL %>
<%= f.rui_input(:banner_image,
type: :url,
label: "Banner Image URL",
placeholder: "https://images.unsplash.com/...",
help_text: "URL to the banner image"
) do |input|
input.with_prefix do %>
<%= rui_icon :image, size: :sm %>
<% end %>
<% end %>
<%# Category %>
<%= f.rui_radio_button(:category,
collection: [
["Technology", "Technology"],
["Design", "Design"],
["Product", "Product"],
["Engineering", "Engineering"],
["Tutorials", "Tutorials"]
],
variant: :pill,
layout: :horizontal,
label: "Category",
color: :primary
) %>
</div>
</div>
Component Excellence:
- Icon prefixes provide visual context (e.g., image icon for URL field)
- Pill variant radio buttons are responsive—horizontal on desktop, stacked on mobile
- Semantic layout using Tailwind grid with gap spacing (no manual margin management)
- Typography component (`rui_text`) handles heading semantics, sizing, and dark mode
3. Author Information Section
A 3-column grid on desktop showing author details. The reading time input includes a suffix unit indicator ("min") for clarity.
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<%= rui_text "Author Information", as: :h3, size: :lg, weight: :semibold, class: "mb-6" %>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<%= f.rui_input(:author_name,
label: "Author Name",
placeholder: "Jane Smith",
help_text: "Display name for the author"
) do |input|
input.with_prefix do %>
<%= rui_icon :user, size: :sm %>
<% end %>
<% end %>
<%= f.rui_input(:author_avatar,
type: :url,
label: "Author Avatar URL",
placeholder: "https://...",
help_text: "URL to avatar image"
) %>
<%= f.rui_input(:reading_time,
type: :number,
label: "Reading Time",
placeholder: "5",
min: 1,
max: 60,
help_text: "Estimated minutes to read"
) do |input|
input.with_suffix do %>
<span class="text-xs text-zinc-500">min</span>
<% end %>
<% end %>
</div>
</div>
Key Insights:
- Input suffixes (`input.with_suffix`) add units without HTML clutter
- Type validation (`:number`) is semantic and enables native browser controls
- Responsive grid (`grid-cols-1 md:grid-cols-3`) adapts from 1 column mobile to 3 desktop
- Help text guides users without needing tooltips
4. Publishing Options Section
Status selection using card-variant radio buttons with descriptions. Checkboxes with variant support (standard vs. switch toggle) for boolean options.
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<%= rui_text "Publishing Options", as: :h3, size: :lg, weight: :semibold, class: "mb-6" %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%# Status %>
<%= f.rui_radio_button(:status,
collection: [
["Draft", "draft", "Save without publishing"],
["Published", "published", "Make visible to readers"],
["Archived", "archived", "Hide from public view"]
],
variant: :card,
description_method: 2,
label: "Status",
color: :primary
) %>
<%# Publish Date %>
<div class="space-y-4">
<%= f.rui_date(:published_at,
label: "Publish Date",
variant: :picker,
help_text: "When to publish (leave blank for now)",
color: :primary
) %>
</div>
</div>
<%# Post Options %>
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-4">
<%= f.rui_checkbox(:featured,
label: "Featured Post",
description: "Show prominently on homepage",
color: :warning
) %>
<%= f.rui_checkbox(:notify_subscribers,
label: "Notify Subscribers",
description: "Email notification on publish",
variant: :switch,
color: :success
) %>
<%= f.rui_checkbox(:allow_comments,
label: "Allow Comments",
description: "Enable reader comments",
color: :info
) %>
</div>
</div>
Design Excellence:
- Card variant radio buttons with descriptions provide more context than dropdowns
- Semantic colors (`color: :warning`, `:success`, `:info`) communicate intent visually
- Checkbox variants (standard vs. switch toggle) adapt to different use cases
- Descriptions on controls reduce the need for external documentation
- Responsive grids automatically adjust layouts for different screen sizes
5. Advanced Scheduling (Optional)
A collapsible `<details>` section hides advanced features by default, keeping the form clean. Developers can optionally set event dates, deadlines, schedule times, and visibility windows.
<details class="border-t border-zinc-200 dark:border-zinc-800 pt-8">
<summary class="cursor-pointer flex items-center gap-2 text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">
<%= rui_icon :calendar, size: :sm %>
<span class="font-medium">Advanced Scheduling</span>
<span class="text-sm">(optional)</span>
</summary>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<%= f.rui_date(:event_date,
label: "Event Date",
help_text: "If this post is about an event"
) %>
<%= f.rui_date(:deadline,
label: "Deadline",
min: Date.today,
help_text: "For time-sensitive content",
color: :danger
) %>
<%= f.rui_date(:schedule_time,
type: :time,
label: "Schedule Time",
help_text: "Specific time to publish"
) %>
<%= f.rui_date(:visibility_period,
variant: :picker,
range: true,
label: "Visibility Window",
help_text: "Auto-show/hide dates"
) %>
</div>
</details>
Progressive Complexity:
- Collapsible details keep the form lean for 80% of users, while power users can expand advanced options
- Date variant flexibility (picker, time, range) supports different temporal needs
- Min attribute on deadline enforces business logic (can't set deadline in the past)
- Visual grouping with responsive grid keeps related fields together
6. Form Actions
Primary actions (save) on the left, destructive actions (delete) on the right. Conditional rendering shows the "Publish Now" link only for draft posts.
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-8 flex items-center justify-between">
<div class="flex items-center gap-3">
<%# Auto-labeled: "Create Post" (new) or "Update Post" (edit) %>
<%= f.rui_button(color: :primary, size: :lg) %>
<%# Publish Now - Only for Drafts %>
<% if post.persisted? && post.status != "published" %>
<%= rui_link publish_post_path(post),
turbo_method: :patch,
color: :success do |link|
link.with_icon(:send, size: :sm)
concat " Publish Now"
end %>
<% end %>
</div>
<div class="flex items-center gap-3">
<%# Delete Button - Only for Persisted Posts %>
<% if post.persisted? %>
<%= rui_button_to "Delete", post_path(post),
method: :delete,
variant: :ghost,
color: :danger,
data: { turbo_confirm: "Are you sure you want to delete this post?" } do |btn|
btn.with_icon(:trash_2, size: :sm)
end %>
<% end %>
<%# Cancel - Always Available %>
<%= rui_link posts_path, variant: :ghost, color: :zinc do |link|
link.with_icon(:x, size: :sm)
concat " Cancel"
end %>
</div>
</div>
Button Strategy:
- Auto-labeled buttons (`f.rui_button`) understand the model state and label themselves correctly
- Semantic colors communicate action intent: primary (save), success (publish), danger (delete)
- Conditional rendering shows/hides actions based on post state (can't publish a published post)
- Icon + text combinations provide clear visual affordance
- Turbo integration (`turbo_method: :patch`, `turbo_confirm`) enables modern interactions
Why RUI Components Are Superior
Here's a concrete comparison: building the "Featured Post" checkbox with vanilla Rails vs. RUI components.
Plain Rails (What You'd Need to Write)
<div class="mb-4">
<label for="post_featured" class="flex items-center gap-2 cursor-pointer">
<input type="hidden" name="post[featured]" value="0">
<input type="checkbox"
id="post_featured"
name="post[featured]"
value="1"
class="w-4 h-4 border-gray-300 rounded">
<div>
<p class="text-sm font-medium text-gray-900">
Featured Post
</p>
<p class="text-xs text-gray-600">
Show prominently on homepage
</p>
</div>
</label>
</div>
14 lines of manual HTML/CSS
RUI Components (What You Actually Write)
<%= f.rui_checkbox(:featured,
label: "Featured Post",
description: "Show prominently on homepage",
color: :warning
) %>
5 lines including layout
That's 3x less code, and the RUI version automatically includes:
- Dark mode support — automatically inherits your theme
- Accessibility — proper ARIA labels and keyboard navigation
- Responsive design — adapts to mobile, tablet, desktop
- Semantic color — `color: :warning` communicates the intent visually
- Hover states — built-in visual feedback on interaction
- Form integration — automatically handles error states and validation
- Consistency — uses the same spacing, colors, and typography as your whole app
Why This Matters for Rails Developers
If you're building production Rails applications, you face a choice: spend hours building and styling forms manually, or use components that work out of the box. This form represents a real use case—a full-featured blog editor with validation, conditional fields, and multiple action states.
For Junior Developers
You don't need to learn CSS-in-JS, Tailwind internals, or HTML form semantics. Write semantic Rails, and RUI components handle the complexity. Focus on your business logic, not styling.
For Experienced Rails Developers
You recognize that this form structure is production-ready, accessible, and respects Rails conventions. No JavaScript shenanigans, no over-engineering. Just solid, composable components.
For Perfectionists
Dark mode works flawlessly. Responsive design is automatic. Color semantics are consistent. Error handling is intelligent. You get a professional UI without fighting CSS for hours.
✨ Next Steps
Try creating a post using the links above. Explore each form field, see how errors appear, test the responsive layout on mobile. This isn't a screenshot—it's a living, breathing Rails form powered by RUI components.
CRUD Action Patterns
Common patterns for Create, Read, Update, Delete operations in Rails.
Show Page with Actions
Display a resource with edit, publish, and delete buttons. Uses semantic colors and RUI link/button components:
<%# app/views/posts/show.html.erb %>
<article class="pb-16">
<%# Article Header with Actions %>
<header class="max-w-3xl mx-auto px-4 py-8">
<div class="flex items-center justify-between mb-6">
<div>
<%= rui_text @post.title, as: :h1, size: :xl5, weight: :bold %>
<%= rui_badge @post.status.capitalize, color: @post.status == "published" ? :success : :zinc, size: :sm %>
</div>
<div class="flex items-center gap-2">
<%= rui_link edit_post_path(@post), color: :primary, size: :sm do |link|
link.with_icon(:edit, size: :sm)
end %>
<% if @post.status != "published" %>
<%= rui_link publish_post_path(@post), turbo_method: :patch, color: :success, size: :sm do |link|
link.with_icon(:send, size: :sm)
end %>
<% end %>
<%= rui_button_to post_path(@post), method: :delete, variant: :ghost, size: :sm, color: :danger do |btn|
btn.with_icon(:trash_2, size: :sm)
end %>
</div>
</div>
</header>
<%# Article Body %>
<div class="max-w-3xl mx-auto px-4 prose prose-zinc dark:prose-invert">
<% if @post.content.present? %>
<%= @post.content %>
<% elsif @post.body.present? %>
<%= simple_format(@post.body) %>
<% end %>
</div>
</article>
Index Page with Card Grid
Display posts in a responsive card grid with badges, metadata, and inline actions:
<%# app/views/posts/index.html.erb %>
<div class="flex items-center justify-between mb-8">
<%= rui_text "Latest Posts", as: :h1, size: :xl, weight: :bold %>
<%= rui_link new_post_path do %>
<%= rui_button "New Post", color: :primary, size: :sm do |btn|
btn.with_icon(:plus, size: :sm)
end %>
<% end %>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" id="posts-list">
<% @posts.each do |post| %>
<article class="group flex flex-col bg-white dark:bg-zinc-900 rounded-xl overflow-hidden border">
<%# Card Image %>
<%= rui_link post_path(post), class: "block aspect-[16/10] overflow-hidden" do %>
<% if post.banner_image.present? %>
<%= rui_image post.banner_image,
alt: post.title,
size: :full,
fit: :cover %>
<% else %>
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br">
<%= rui_icon :image, class: "w-12 h-12 text-zinc-400" %>
</div>
<% end %>
<% end %>
<%# Card Content %>
<div class="flex flex-col flex-1 p-5">
<%# Badges %>
<div class="flex items-center gap-2 mb-3">
<% if post.category.present? %>
<%= rui_badge post.category, color: :primary, size: :xs %>
<% end %>
<% if post.status != "published" %>
<%= rui_badge post.status.capitalize, color: :zinc, size: :xs %>
<% end %>
</div>
<%# Title %>
<%= rui_link post_path(post) do %>
<%= rui_text post.title, as: :h3, weight: :bold %>
<% end %>
<%# Excerpt %>
<% if post.excerpt.present? %>
<p class="text-zinc-600 dark:text-zinc-400 text-sm mb-4 line-clamp-2 flex-1">
<%= post.excerpt %>
</p>
<% end %>
<%# Meta %>
<div class="flex items-center justify-between pt-4 border-t">
<div class="flex items-center gap-2">
<% if post.author_avatar %>
<%= rui_avatar src: post.author_avatar, size: :sm %>
<% end %>
<span class="text-xs text-zinc-500">
<%= post.published_at&.strftime("%b %d, %Y") %>
</span>
</div>
<% if post.reading_time %>
<span class="text-xs text-zinc-500 flex items-center gap-1">
<%= rui_icon :clock, size: :xs %>
<%= post.reading_time %> min
</span>
<% end %>
</div>
</div>
<%# Hover Actions %>
<div class="px-5 pb-5 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<%= rui_link edit_post_path(post), variant: :ghost, size: :xs, color: :zinc do |link|
link.with_icon(:edit, size: :xs)
end %>
<%= rui_button_to post_path(post),
method: :delete,
variant: :ghost,
size: :xs,
color: :danger do |btn|
btn.with_icon(:trash_2, size: :xs)
end %>
</div>
</article>
<% end %>
</div>
Bulk Actions
Enable multiple item selection with bulk delete:
<%= form_with url: bulk_delete_posts_path, method: :delete do |f| %>
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">Posts</h1>
<%= f.rui_button("Delete Selected", color: :danger, variant: :outline) %>
</div>
<div class="space-y-2">
<% @posts.each do |post| %>
<label class="flex items-center gap-3 p-4 bg-white dark:bg-zinc-900 border rounded-lg hover:border-zinc-300">
<%= f.check_box :post_ids, { multiple: true }, post.id, nil %>
<span><%= post.title %></span>
</label>
<% end %>
</div>
<% end %>
Authentication Patterns
Use social buttons and form patterns for authentication flows.
Social Authentication
Login page with social providers (works with Devise, OmniAuth, etc.):
<div class="max-w-md mx-auto mt-12">
<h1 class="text-3xl font-bold text-center mb-8">Sign In</h1>
<div class="space-y-3 mb-8">
<%= rui_social_button(:google, path: "/auth/google_oauth2", class: "w-full") %>
<%= rui_social_button(:github, path: "/auth/github", class: "w-full") %>
<%= rui_social_button(:facebook, path: "/auth/facebook", class: "w-full") %>
</div>
<div class="relative mb-8">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-zinc-200 dark:border-zinc-800"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white dark:bg-zinc-950 text-zinc-500">Or continue with email</span>
</div>
</div>
<%= form_with url: session_path(resource_name), class: "space-y-6" do |f| %>
<div>
<%= f.label :email, class: "block text-sm font-medium mb-2" %>
<%= f.email_field :email, autofocus: true, class: "w-full px-3 py-2 border rounded-lg" %>
</div>
<div>
<%= f.label :password, class: "block text-sm font-medium mb-2" %>
<%= f.password_field :password, class: "w-full px-3 py-2 border rounded-lg" %>
</div>
<%= f.rui_button("Sign In", color: :primary, class: "w-full") %>
<% end %>
</div>
Payment Integration
Checkout page with payment provider buttons:
<div class="max-w-md mx-auto">
<h2 class="text-2xl font-bold mb-6">Choose Payment Method</h2>
<div class="space-y-3">
<%= rui_social_button(:paypal, path: checkout_paypal_path, class: "w-full") %>
<%= rui_social_button(:stripe, path: checkout_stripe_path, class: "w-full") %>
<%= rui_social_button(:apple, path: checkout_apple_pay_path, class: "w-full") %>
</div>
</div>
List View Patterns
Common patterns for displaying collections of resources.
Table with Row Actions
Display data in a table with inline action buttons:
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-zinc-50 dark:bg-zinc-900">
<tr>
<th class="px-4 py-3 text-left">Title</th>
<th class="px-4 py-3 text-left">Status</th>
<th class="px-4 py-3 text-left">Created</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-800">
<% @posts.each do |post| %>
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-900">
<td class="px-4 py-3"><%= post.title %></td>
<td class="px-4 py-3"><%= post.status %></td>
<td class="px-4 py-3"><%= post.created_at.to_date %></td>
<td class="px-4 py-3">
<div class="flex justify-end gap-2">
<%= link_to post_path(post) do %>
<%= rui_button(size: :sm, variant: :ghost) do |button| %>
button.with_icon(name: :eye)
<% end %>
<% end %>
<%= link_to edit_post_path(post) do %>
<%= rui_button(size: :sm, variant: :ghost) do |button| %>
button.with_icon(name: :edit)
<% end %>
<% end %>
<%= rui_button_to(post_path(post), method: :delete, size: :sm, variant: :ghost, color: :danger) do |button|
button.with_icon(name: :trash)
end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
Card Grid with Actions
Display items in a responsive card grid:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @products.each do |product| %>
<div class="bg-white dark:bg-zinc-900 border rounded-lg overflow-hidden">
<img src="<%= product.image_url %>" alt="<%= product.name %>" class="w-full h-48 object-cover" />
<div class="p-4">
<h3 class="text-lg font-semibold mb-2"><%= product.name %></h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-4"><%= product.description %></p>
<div class="flex items-center justify-between">
<span class="text-xl font-bold"><%= number_to_currency(product.price) %></span>
<%= rui_button_to("Add to Cart", cart_items_path(product_id: product.id), method: :post, color: :primary, size: :sm) do |button|
button.with_icon(name: "shopping-cart", position: :leading)
end %>
</div>
</div>
</div>
<% end %>
</div>
Turbo Integration
RapidRails UI components work seamlessly with Hotwire Turbo for dynamic interactions.
Turbo Frame Modal
Load forms in a modal using Turbo Frames:
<%= link_to new_post_path, data: { turbo_frame: "modal" } do %>
<%= rui_button("New Post", color: :primary) do |button| %>
button.with_icon(name: :plus, position: :leading)
<% end %>
<% end %>
<%= turbo_frame_tag "modal" %>
<%= turbo_frame_tag "modal" do %>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center">
<div class="bg-white dark:bg-zinc-900 rounded-lg p-6 max-w-2xl w-full">
<h2 class="text-2xl font-bold mb-6">New Post</h2>
<%= form_with model: @post do |f| %>
<div class="flex gap-3">
<%= f.rui_button(color: :primary) %>
<%= link_to "Cancel", posts_path, class: "..." %>
</div>
<% end %>
</div>
</div>
<% end %>
Inline Editing with Turbo Frames
Edit records inline without page reload:
<%= turbo_frame_tag dom_id(post) do %>
<div class="flex items-center justify-between p-4 border rounded-lg">
<div>
<h3 class="font-semibold"><%= post.title %></h3>
<p class="text-sm text-zinc-600"><%= post.body %></p>
</div>
<%= link_to edit_post_path(post) do %>
<%= rui_button("Edit", size: :sm, variant: :outline) %>
<% end %>
</div>
<% end %>
<%= turbo_frame_tag dom_id(@post) do %>
<%= form_with model: @post, class: "p-4 border rounded-lg" do |f| %>
<%= f.text_field :title, class: "w-full mb-3 px-3 py-2 border rounded-lg" %>
<%= f.text_area :body, class: "w-full mb-3 px-3 py-2 border rounded-lg" %>
<div class="flex gap-2">
<%= f.rui_button("Save", color: :primary, size: :sm) %>
<%= link_to "Cancel", post_path(@post), class: "..." %>
</div>
<% end %>
<% end %>
Pro tip: RapidRails UI components automatically work with Turbo's loading states and disabled states during form submission.