rails-viewcomponents
Skill2 starsChanged 8 days agoMITView on GitHub
Use when creating, extracting, or refactoring ViewComponents in a Rails app — including slot design, component API, testing, and when to extract vs keep as partials
1 other project uses a version of this.
Adopted by
Most recently taken up by
- mattsears/rails-ctotheir own version, first seen Apr 25, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- mattsears/rails-cto★ 15same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository24 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- rails-activestorageSkill
- rails-apiSkill
- rails-auditSkill
- rails-clean-test-outputSkill
- rails-controllersSkill
- rails-coreSkill
- rails-database-performanceSkill
- rails-i18nSkill
- rails-jobsSkill
- rails-loggingSkill
- rails-mailersSkill
- rails-migrationsSkill
- rails-modelsSkill
- rails-multi-tenancySkill
- rails-performanceSkill
- rails-philosophySkill
- rails-project-setupSkill
- rails-securitySkill
- rails-stimulusSkill
- rails-styleSkill
- rails-testingSkill
- rails-turboSkill
- rails-upgradeSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 5 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 90
Never put logic inline in the template.
- Very specific: line 91
Avoid coupling to global state (request params, Current, URLs) — pass everything via the constructor
- Very specific: line 92
Use -Component suffix in class name
- Very specific: line 93
If the app supports dark mode, style both modes: Every new view or component needs light *and* dark styling (Tailwind dark: variants) — …
- Very specific: line 135
Always guard with header? / actions?
- Very specific: line 136
Use block (with_*) syntax, not string arguments, for HTML content
- Distinctive: line 141
Never subclass a component to vary behaviour.
- Distinctive: line 183
Test rendered output, not instance methods
- Very specific: line 184
Use assert_selector / assert_text (Capybara matchers)
- Distinctive: line 186
Don't assert on private method return values
- Very specific: line 212
Keep previews in the configured preview path (commonly test/components/previews/).
- Distinctive: line 213
Add the preview in the same commit as the component so the cop stays green.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rails ViewComponents
Overview
ViewComponents are Ruby objects that render HTML. Think of them as "ActiveRecord for UI" — they bring testability, explicit interfaces, and reuse to view code that would otherwise be scattered across partials.
When to Use
- Replacing a partial that has grown logic or conditional rendering
- A UI pattern appears in 3+ places (three-instance rule before extracting)
- The view needs unit-testable behaviour
- You want an explicit, typed interface instead of implicit local assigns
Keep as a partial when: it's simple, single-use, and has no logic.
Component Types
| Type | Purpose | Example |
| General-purpose | Reusable UI pattern | ButtonComponent, CardComponent |
| Domain-specific | Wraps a model into a general component | User::AvatarComponent → DesignSystem::AvatarComponent |
Extract general-purpose components only after they're proven useful in multiple contexts — "good frameworks are extracted, not invented."
File Structure
app/components/
button_component.rb
button_component.html.erb
user/
avatar_component.rb
avatar_component.html.erbDefining a Component
Make the component the single source of truth for its styling: keep the class map in a
constant and resolve it through one method — never scatter btn btn-* shims across views.
(Extracting a component is the primary cure for utility-class soup — see the dev-hooks
tailwind skill for design tokens, dark-mode pairing, and when to reach for @apply.)
class ButtonComponent < ViewComponent::Base
VARIANT_CLASSES = {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
danger: "bg-red-600 text-white hover:bg-red-700"
}.freeze
SIZE_CLASSES = { sm: "px-2 py-1 text-sm", md: "px-3 py-2", lg: "px-5 py-3 text-lg" }.freeze
# Class method so OTHER components' templates (which don't get view helpers)
# can reuse the styling without instantiating a ButtonComponent.
def self.classes_for(variant: :primary, size: :md)
[ "btn-base", VARIANT_CLASSES.fetch(variant), SIZE_CLASSES.fetch(size) ].join(" ")
end
def initialize(label:, variant: :primary, size: :md, disabled: false)
@label = label
@variant = variant
@size = size
@disabled = disabled
end
private
def css_classes
[ self.class.classes_for(variant: @variant, size: @size), ("opacity-50" if @disabled) ].compact.join(" ")
end
end<%# button_component.html.erb %>
<button class="<%= css_classes %>"<%= " disabled".html_safe if @disabled %>>
<%= @label %>
</button>To change a colour or add a variant, edit only VARIANT_CLASSES — that's the whole point
of the single-source-of-truth pattern.
Rules:
- Mark all instance methods
private— they're still accessible in the template - Never put logic inline in the template; push it into instance methods
- Avoid coupling to global state (request params,
Current, URLs) — pass everything via the constructor - Use
-Componentsuffix in class name - If the app supports dark mode, style both modes. Every new view or component needs light and dark styling (Tailwind
dark:variants) — check an existing component for the app's convention before shipping a light-only one. - ViewComponents do not auto-include view helpers. Inside a component template you can't call app helpers (
btn_classes,get_link, etc.) — those exist only in regular views. When one component needs another's styling, expose it as a class method (ButtonComponent.classes_for(...)) and call that instead.
Slots
Slots let callers inject structured content. Prefer slots over passing HTML strings (which bypass Rails sanitisation).
class CardComponent < ViewComponent::Base
renders_one :header
renders_one :footer
renders_many :actions, ActionComponent
end<%# card_component.html.erb %>
<div class="card">
<% if header? %>
<div class="card-header"><%= header %></div>
<% end %>
<div class="card-body"><%= content %></div>
<% if actions? %>
<div class="card-actions">
<% actions.each { |a| concat(a) } %>
</div>
<% end %>
</div>Caller:
<%= render CardComponent.new do |c| %>
<% c.with_header { "My Title" } %>
<% c.with_action(label: "Save") %>
<% end %>Slot rules:
renders_one— at most one instancerenders_many— zero or more; iterate in the template- Always guard with
header?/actions?predicate before rendering - Use block (
with_*) syntax, not string arguments, for HTML content - Lambda slots are fine for trivial wrapping; extract to a component when they grow
Composition over Inheritance
Never subclass a component to vary behaviour. Wrap it instead:
# Bad
class DangerButtonComponent < ButtonComponent; end
# Good
class DangerButtonComponent < ViewComponent::Base
def initialize(label:)
@button = ButtonComponent.new(label:, variant: :danger)
end
endTesting
class ButtonComponentTest < ViewComponent::TestCase
def test_renders_label
render_inline(ButtonComponent.new(label: "Save"))
assert_text "Save"
end
def test_disabled_state
render_inline(ButtonComponent.new(label: "Save", disabled: true))
assert_selector "button.disabled"
end
endTesting slots:
def test_card_with_header
render_inline(CardComponent.new).tap do |c|
c.with_header { "Hello" }
end
assert_selector ".card-header", text: "Hello"
endRules:
- Test rendered output, not instance methods
- Use
assert_selector/assert_text(Capybara matchers) assert_selectorhides invisible elements by default — passvisible: falsefor hidden content- Don't assert on private method return values
Previews
Every component needs a preview. Generate a *_component_preview.rb alongside the
component — many projects have a lint cop that fails the build for a component without one,
and previews double as living documentation in the ViewComponent UI / Lookbook.
# test/components/previews/button_component_preview.rb
class ButtonComponentPreview < ViewComponent::Preview
def primary
render ButtonComponent.new(label: "Save")
end
def danger
render ButtonComponent.new(label: "Delete", variant: :danger)
end
def disabled
render ButtonComponent.new(label: "Save", disabled: true)
end
end- One public method per state/variant you want to showcase.
- Keep previews in the configured preview path (commonly
test/components/previews/). - Add the preview in the same commit as the component so the cop stays green.
Quick Reference
| Task | Pattern |
| Define argument | def initialize(foo:, bar: nil) |
| Single slot | renders_one :header |
| Repeating slot | renders_many :items, ItemComponent |
| Slot with component | renders_one :icon, IconComponent |
| Check slot presence | header?, items? |
| Render slot | <%= header %> / <% items.each { |i| concat(i) } %> |
| Caller sets slot | c.with_header { "text" } |
| Private helper | private def css_classes = ... |
| Reuse styling across components | def self.classes_for(variant:, size:) = ... |
| Preview a component | test/components/previews/x_component_preview.rb |
Common Mistakes
| Mistake | Fix |
| Passing HTML strings to slots | Use block syntax: c.with_header { "<b>text</b>".html_safe } |
| Inheriting to vary style | Compose — pass a wrapper or different arguments |
Reading params / Current inside component | Inject via constructor |
| Asserting instance methods in tests | Assert rendered HTML output |
| Extracting on first use | Wait for the third instance before generalising |
Logic inline in .erb | Move to a private instance method |
| Calling a view helper inside a component template | Helpers aren't auto-included — expose a class method like classes_for and call that |
Scattering btn btn-* style classes across views | One component owns the class map in a constant; edit only that constant |
| Shipping a component without a preview | Add a *_component_preview.rb in the same commit (a lint cop often requires it) |
| Light-only styling in a dark-mode app | Add dark: variants alongside every light style |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

