rails-components
Expert guidance for building reusable UI components in Rails using partials, CSS classes, and helpers. Use when creating components, partials, reusable UI patterns, empty states, modals, cards, buttons, badges, alerts, data tables, or any component pattern. Covers decision framework for CSS-only vs partial vs helper vs ViewComponent, proper use of local_assigns, block yielding, capture for slots, variant patterns, and component galleries.
How do I install this agent skill?
npx skills add https://github.com/thinkoodle/rails-skills --skill rails-componentsIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The rails-components skill provides expert guidance and templates for creating reusable UI components in Ruby on Rails. It adheres to standard Rails development practices and includes discovery steps to ensure consistency with existing codebases. No security issues, malicious code, or unauthorized data access patterns were detected.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
Rails Component Patterns
Build reusable, self-contained, composable UI components using Rails primitives — partials, CSS classes, and helpers.
Philosophy
Core Principles:
- Start with CSS classes — don't reach for a partial when a class will do
- Self-contained — each component owns its markup AND styles in one place
- Composable — components nest and combine naturally
- Consistent — same component = same markup = same appearance everywhere
- Progressive complexity — graduate from CSS → partial → helper only when needed
The Component Ladder:
ViewComponent (gem) ← Complex: encapsulated Ruby + template + tests
Helper method ← Frequent: shorthand for common partials
Partial with locals ← Structured: logic, slots, complex markup
CSS classes only ← Simple: single element, few variants
Always start at the bottom. Move up only when you feel pain.
When To Use This Skill
- Building new reusable UI components (buttons, cards, modals, etc.)
- Deciding between CSS classes, partials, helpers, or ViewComponent
- Creating empty states, alerts, badges, or data tables
- Refactoring repeated markup into shared components
- Setting up a component gallery for development
- Implementing variant patterns (sizes, colors, states)
- Organizing component files and styles
Decision Framework
Use CSS Classes When:
- Component is a single element or simple wrapper (
<button>,<span>,<div>) - Variants are purely visual (color, size, spacing)
- No conditional logic needed
- No content slots beyond inner text
Examples: buttons, badges, simple cards, form inputs, dividers
Use Partials When:
- Component has conditional sections (show/hide based on args)
- Multiple named content areas (header, body, footer)
- Non-trivial markup structure (5+ elements)
- Default values or computed attributes needed
Examples: empty states, modals, complex cards, data tables, alerts with icons
Use Helpers When:
- A partial is used very frequently (10+ call sites)
- You want a cleaner Ruby API:
ui_badge("Active", variant: :success) - Simple components where ERB render syntax feels heavy
- You want to compose multiple elements in Ruby
Examples: badges, status indicators, icon buttons, breadcrumbs
Use ViewComponent When:
- Component needs its own unit tests
- Complex state logic that doesn't belong in a view
- Team is large and components need strict interfaces
- You're building a design system with dozens of components
Skip ViewComponent for most Rails apps. Partials + helpers cover 90% of needs.
Instructions
Step 1: Check Existing Components
ALWAYS look for existing patterns first:
# Find existing component partials
find app/views -path "*/components/*" -o -path "*/_component*" | head -20
# Find component CSS
find app/assets -name "*component*" -o -path "*/components/*" | head -20
# Find component helpers
rg "def ui_|def component_" app/helpers/
# Check for ViewComponent usage
ls app/components/ 2>/dev/null
rg "ViewComponent" Gemfile
Match existing project conventions. Consistency > your preference.
Step 2: Choose the Right Approach
Reference the decision framework above. Ask:
- Can this be done with just CSS classes? → Do that.
- Does it need conditional logic or slots? → Partial.
- Is it called from 10+ places with the same pattern? → Add a helper.
- Does it need isolated tests and complex state? → ViewComponent.
Step 3: File Organization
app/views/components/ # Shared component partials
├── _modal.html.erb
├── _empty_state.html.erb
├── _card.html.erb
├── _alert.html.erb
└── _data_table.html.erb
app/assets/stylesheets/components/ # One CSS file per component
├── buttons.css
├── badges.css
├── cards.css
├── modals.css
├── empty-states.css
├── alerts.css
└── tables.css
app/helpers/
└── component_helper.rb # Helper shortcuts for common components
Naming conventions:
- Partials:
_snake_case.html.erbinapp/views/components/ - CSS files:
kebab-case.cssinapp/assets/stylesheets/components/ - Helpers:
ui_component_nameprefix inComponentHelper - CSS classes:
.component-name,.component-name-element,.component-name--variant
Step 4: Build CSS-Only Components
For simple components, define CSS classes and use directly in templates:
/* app/assets/stylesheets/components/buttons.css */
@layer components {
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-primary { background: var(--color-primary); color: white; }
.btn-secondary { background: var(--color-surface); border: 1px solid var(--color-border); }
.btn-danger { background: var(--color-danger); color: white; }
.btn-ghost { background: transparent; }
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
.btn-lg { padding: 0.75rem 1.5rem; }
.btn-icon { padding: 0.5rem; }
}
<%# Usage — no partial needed %>
<button class="btn btn-primary">Save</button>
<button class="btn btn-danger btn-sm">Delete</button>
<%= link_to "View", post_path(@post), class: "btn btn-secondary" %>
Step 5: Build Partial-Based Components
Every partial MUST have a comment block documenting its interface:
<%# app/views/components/_empty_state.html.erb %>
<%#
Empty State Component
Arguments:
icon: (String) Icon name — required
title: (String) Main heading — required
description: (String, optional) Supporting text
class: (String, optional) Additional CSS classes
Block: Optional action buttons
%>
<div class="empty-state <%= local_assigns[:class] %>">
<div class="empty-state-icon">
<%= lucide_icon icon, size: 48 %>
</div>
<h3 class="empty-state-title"><%= title %></h3>
<% if local_assigns[:description] %>
<p class="empty-state-description"><%= description %></p>
<% end %>
<% if block_given? %>
<div class="empty-state-actions">
<%= yield %>
</div>
<% end %>
</div>
Key rules for partials:
- Use
local_assigns[:key]to check for optional args — NOTdefined?orif key - Use
||=for defaults when the arg should have a fallback value - Use
block_given?to check if a block was passed - Use
yieldfor the primary content slot - Use
capturefor additional named slots
Step 6: Implement Slots with Capture
For components with multiple content areas:
<%# app/views/components/_card.html.erb %>
<%#
Card Component
Arguments:
header: (String|HTML, optional) Header content — use capture for complex HTML
footer: (String|HTML, optional) Footer content — use capture for complex HTML
class: (String, optional) Additional CSS classes
variant: (Symbol, optional) Card variant — :default, :stat, :interactive
Block: Card body content (required)
%>
<%
variant ||= :default
css_class = ["card", ("card-#{variant}" unless variant == :default), local_assigns[:class]].compact.join(" ")
%>
<div class="<%= css_class %>">
<% if local_assigns[:header] %>
<div class="card-header"><%= header %></div>
<% end %>
<div class="card-body">
<%= yield %>
</div>
<% if local_assigns[:footer] %>
<div class="card-footer"><%= footer %></div>
<% end %>
</div>
Caller uses capture for rich slot content:
<%= render "components/card",
header: capture { tag.h3("Settings", class: "card-title") },
footer: capture {
link_to("Cancel", "#", class: "btn btn-secondary") +
link_to("Save", "#", class: "btn btn-primary")
} do %>
<p>Card body content here.</p>
<% end %>
Step 7: Add Helper Shortcuts
For frequently-used components, create helpers:
# app/helpers/component_helper.rb
module ComponentHelper
def ui_badge(text, variant: :muted)
tag.span(text, class: "badge badge-#{variant}")
end
def ui_empty_state(title:, icon: nil, description: nil, &block)
render("components/empty_state",
title: title,
icon: icon,
description: description,
&block)
end
def ui_alert(variant: :info, dismissible: false, &block)
render("components/alert",
variant: variant,
dismissible: dismissible,
&block)
end
end
Usage becomes clean:
<%= ui_badge "Active", variant: :success %>
<%= ui_badge "Draft" %>
<%= ui_empty_state(
icon: "inbox",
title: "No messages",
description: "Check back later") %>
Step 8: Handle Variants
CSS variants for purely visual changes:
/* Size variants */
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
.btn-lg { padding: 0.75rem 1.5rem; }
/* State variants */
.card-interactive:hover { border-color: var(--color-border-strong); }
.card-flush .card-body { padding: 0; }
Partial argument variants when markup changes:
<%# In the partial %>
<%
variant ||= :info
icon_name = { success: "check-circle", error: "alert-circle",
warning: "alert-triangle", info: "info" }[variant.to_sym]
%>
<div class="alert alert-<%= variant %>" role="alert">
<%= lucide_icon icon_name, size: 18 %>
<span class="alert-content"><%= yield %></span>
</div>
Step 9: Create a Component Gallery
Essential for development. Let anyone browse all components:
# config/routes.rb
if Rails.env.development?
get "components", to: "components#index"
end
# app/controllers/components_controller.rb
class ComponentsController < ApplicationController
layout "application"
def index
end
end
<%# app/views/components/index.html.erb %>
<div style="max-width: 48rem; margin: 2rem auto; padding: 0 1rem;">
<h1>Component Gallery</h1>
<h2>Buttons</h2>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 2rem;">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-sm">Small</button>
</div>
<h2>Badges</h2>
<div style="display: flex; gap: 0.5rem; margin-bottom: 2rem;">
<span class="badge badge-muted">Draft</span>
<span class="badge badge-success">Active</span>
<span class="badge badge-warning">Pending</span>
<span class="badge badge-danger">Failed</span>
</div>
<h2>Empty States</h2>
<%= render "components/empty_state",
icon: "inbox",
title: "No messages yet",
description: "When you receive messages, they'll appear here." do %>
<button class="btn btn-primary">Compose</button>
<% end %>
<h2>Alerts</h2>
<%= render "components/alert", variant: :info do %>Info message<% end %>
<%= render "components/alert", variant: :success do %>Success message<% end %>
<%= render "components/alert", variant: :warning do %>Warning message<% end %>
<%= render "components/alert", variant: :error do %>Error message<% end %>
</div>
Anti-Patterns
- Partial for everything — A
<span class="badge">does NOT needrender "components/badge". CSS class is enough. - Using
defined?(variable)— Always uselocal_assigns[:variable]in partials.defined?is unreliable. - Global styles for component-specific things — Component CSS lives in
components/. Don't polluteapplication.css. - Inconsistent naming — Pick a convention and stick with it.
_empty_state.html.erbnot_emptyState.html.erb. - God components — If a partial takes 10+ arguments, split it into smaller components.
- Missing documentation — Every partial needs a comment block listing its arguments.
- Passing HTML strings as args — Use
capture { }blocks oryield, never raw HTML strings. - No gallery page — If you can't see all your components in one place, they'll diverge.
CSS Conventions
/* Use @layer for specificity management */
@layer components {
.component-name { /* base styles */ }
.component-name-element { /* child element */ }
.component-name--variant { /* BEM variant — OR use component-variant */ }
}
Design token usage:
.component {
color: var(--color-ink); /* Text colors */
background: var(--color-surface); /* Backgrounds */
border: 1px solid var(--color-border); /* Borders */
padding: var(--space-4); /* Spacing scale */
font-size: var(--text-sm); /* Type scale */
border-radius: var(--radius-md); /* Border radius */
}
If the project uses design tokens/CSS custom properties, use them. If not, use consistent raw values.
Quick Reference
local_assigns Cheat Sheet
<%# Check if argument was passed (even if nil) %>
<% if local_assigns.key?(:title) %>
<%# Check if argument was passed AND is truthy %>
<% if local_assigns[:title] %>
<%# Default value %>
<% variant = local_assigns[:variant] || :default %>
<%# OR %>
<% variant ||= :default %>
<%# Pass-through CSS classes %>
<div class="component <%= local_assigns[:class] %>">
Render Syntax
<%# Simple — no block %>
<%= render "components/empty_state", icon: "inbox", title: "Empty" %>
<%# With block %>
<%= render "components/modal", title: "Confirm" do %>
<p>Are you sure?</p>
<% end %>
<%# With capture slots %>
<%= render "components/card",
header: capture { tag.h3("Title") },
footer: capture { tag.button("Save", class: "btn btn-primary") } do %>
<p>Body</p>
<% end %>
Common Component Templates
See reference.md in this skill directory for complete templates of:
- Buttons, Badges (CSS-only)
- Cards, Empty States, Alerts (partial)
- Modals with Stimulus (partial + JS)
- Data Tables (partial)
- Helper methods for all of the above
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/thinkoodle/rails-skills/rails-components">View rails-components on skillZs</a>