ViewComponent + Tailwind: The Omakase Stack Nobody Asked For
DHH talks a lot about the “omakase” approach - the chef’s choice, the defaults that work. But sometimes the chef’s choice needs a little customization.
The Stack
- Rails 7.1 (the omakase foundation)
- ViewComponent (for component architecture)
- Tailwind CSS (for styling)
- Hotwire (for interactivity)
Nothing fancy. Nothing trendy. Just solid tools that work.
Why ViewComponent?
Because partials get messy. Because helpers are a dumping ground. Because components are just better.
A ViewComponent is just a Ruby class:
class StatCardComponent < ViewComponent::Base
def initialize(value:, label:, glow: false)
@value = value
@label = label
@glow = glow
end
end
And a template:
<div class="stat-card">
<span class="stat-value <%= 'glow' if @glow %>">
<%= @value %>
</span>
<span class="stat-label">
<%= @label %>
</span>
</div>
That’s it. No magic. No DSL. Just a class and a template.
Why Tailwind?
Because I got tired of naming things. Because flex items-center gap-4 is clearer than .header-nav-container-flex.
Yes, the classes are verbose. But they’re also:
- Searchable
- Self-documenting
- Easy to modify
- Impossible to create cascade issues with
The Component Library
After 5 weeks, I had 43 components:
Layout Components:
- Container
- Header
- Footer
- Sidebar
- Grid
UI Components:
- Button (with 6 variants)
- StatCard
- Table
- Form
- Modal
- Alert
Interactive Components:
- Dropdown
- Tabs
- Accordion
- Search
- Filter
Each one perfectly styled with the terminal aesthetic. Each one tested. Each one documented.
The Pattern
Every component follows the same pattern:
- Ruby class - Handles logic and state
- ERB template - Handles markup
- Tailwind classes - Handles styling
- Test - Ensures it works
No magic. No abstractions. Just clear, maintainable code.
What I Learned
ViewComponent is underrated. It gives you the component architecture of React without leaving Rails.
Tailwind is divisive for good reason. You either love it or hate it. I love it because I hate naming things.
The omakase philosophy is a starting point. Take the defaults, then make them yours.
43 components is not too many. Each one has a purpose. Each one sparks joy.
The Alternative
I could have used a pre-built admin framework. Avo. ActiveAdmin. Administrate.
But then I wouldn’t own it. I wouldn’t understand every line. I wouldn’t be able to make it glow green.
The Code
Want to build your own? Start here:
# app/components/base_component.rb
class BaseComponent < ViewComponent::Base
# Your base styling and behavior
end
# app/components/button_component.rb
class ButtonComponent < BaseComponent
def initialize(variant: :primary, **options)
@variant = variant
@options = options
end
def classes
base = "px-4 py-2 transition-all duration-300"
variant_class = variant_classes[@variant]
"#{base} #{variant_class}"
end
private
def variant_classes
{
primary: "bg-black border border-green-500 text-green-500 hover:shadow-glow",
secondary: "bg-black border border-green-700 text-green-700"
}
end
end
Then build from there. One component at a time. Make each one yours.
The Result
A component library that:
- Works exactly how I need it to
- Looks exactly how I want it to
- Makes me smile every time I use it
That’s the omakase stack nobody asked for, but everyone should consider.
Commit #492 was when the component library felt “complete”. It wasn’t, of course. It never is.