Maintaining a consistent UI in a growing Rails application is a classic challenge. We often start with the best intentions, clean HTML and utility classes, but as the app scales, we inevitably fall into "UI boilerplate fatigue." Whether it’s copy-pasting the same "Avatar with initials" logic across dozens of views or reinventing the wheel for every animated toast, this duplication slowly erodes our development velocity.
To solve this, I recently migrated Calcpace, a running and cycling tracker built with Rails 8, to jet_ui, JetRockets’ component library. In this article, I’ll show you how we used Calcpace as a real-world playground to standardize our interface, leverage Tailwind CSS v4, and solve the production "gotchas" that often come with gem-based assets
The Problem: Copy-Paste Debt
Before the migration, our UI was functional but repetitive. Handling profile pictures required manual conditional logic for avatars and initials in every view:
This "inline Tailwind" approach lacks a single source of truth. Changing a border radius meant a tedious search-and-replace across the entire codebase.
Before jet_ui
After jet_ui
The Strategy: Incremental Migration
One of the biggest concerns when adopting a component library is the "big bang" rewrite. Do you have to change every view at once? Absolutely not.
`jet_ui` is designed for incremental adoption. In Calcpace, we didn't touch our legacy views initially. We started by replacing the most "noisy" elements—flashes and avatars—and then moved to complex data tables. You can have a page powered entirely by `jet_ui` components sitting right next to a legacy ERB view using plain Tailwind utility classes. They coexist perfectly because `jet_ui` respects your existing Tailwind configuration while providing the structure of ViewComponent. This removes the psychological barrier of migration: you can improve your app one component at a time.
Requirements and Plugging in Jet UI
`jet_ui` is built on ViewComponent and Tailwind CSS v4. It follows a "Rails-native" philosophy, leveraging the latest tools in the ecosystem:
- Ruby >= 3.0 and Rails >= 7.0 (Calcpace runs on Rails 8.1).
One of jet_ui's standout features is its suite of generators. They don’t just copy files; they wire up your entire application.
jet_ui:install
This sets up the library in your application (CSS + JS). It is safe to re-run after gem upgrades, as already-configured steps are automatically skipped:
# Install or update
rails generate jet_ui:install
jet_ui:eject
If you need to customize a component beyond standard options, you can "eject" it. This copies the Ruby class, ERB template, and Stimulus controller directly into your `app/components/jet_ui/` folder. The ejected files take precedence automatically.
You can eject multiple components at once and use flags to keep your codebase lean:
# Eject button, card, and flash
rails generate jet_ui:eject btn card flash
# Skip specific files if you only want to customize the template
rails generate jet_ui:eject btn --skip-test --skip-preview
rails generate jet_ui:eject flash --skip-javascript
Production Readiness: The Vendoring Strategy
While the `install` generator works perfectly for local development by pointing to the gem's path, production environments like Docker or CI require a more portable approach. To ensure a deterministic build and clean logs, we adopt a Vendoring strategy. Instead of relying on absolute filesystem paths that change between environments, we copy the CSS directly into the repository but place it outside the standard Rails asset search path to avoid duplicate serving.
1. Vendor the assets programmatically:
mkdir -p vendor/stylesheets
cp -r $(bundle show jet_ui)/app/assets/stylesheets/* vendor/stylesheets/
- Portability: The build works in Docker, CI, and any developer's machine without modifications.
- Clean Logs: By placing files in `vendor/stylesheets` (which Propshaft ignores by default), the asset pipeline won't try to serve individual component files (like `popover.css`). This prevents the "404 Not Found" noise in production logs for files already bundled into your main CSS.
Before jet_ui
After jet_ui
Customizing the Theme with Tailwind v4
`jet_ui` uses modern CSS variables. Instead of overriding thousands of utility classes, you update the theme variables in your CSS source:
Every component, from buttons to focus rings, will now use your custom palette.
Replacing the Noise: Real-World Examples
Interactive Form Groups
We used `jet_ui.group` to standardize selectors. For our activity unit toggle (KM/MI), the component handles the styling and layout, leaving us with a clean DSL:
<%= jet_ui.group do %>
<%= form.radio_button :unit, "km", checked: true %>
<%= form.radio_button :unit, "mi" %>
<% end %>
Advanced Composition: Tables and Tabs
The World Records page was our "stress test" for displaying dense data. By composing `tabs`, `card`, and `table`, we reduced a complex view to a readable DSL:
<%= jet_ui.tabs do %>
<%= jet_ui.tabs_item "KM", href: records_path(unit: "km"), active: @unit == "km" %>
<%= jet_ui.tabs_item "MI", href: records_path(unit: "mi"), active: @unit == "mi" %>
<% end %>
<%= jet_ui.card class: "overflow-hidden" do %>
<%= jet_ui.table hovered: true do %>
<%= jet_ui.table_thead do %>
<%= jet_ui.table_tr do %>
<%= jet_ui.table_th { "Event" } %>
<%# ... %>
<% end %>
<% end %>
<% end %>
<% end %>
This composition proves the architectural leverage: we get a professional data grid with integrated navigation, all following the same design system with zero manual CSS. It shows that `jet_ui` isn't just for simple widgets, but for the core, data-rich parts of your application.
World Records table using jet_ui
Why jet_ui? Comparing with alternatives
You might ask: "Why not just use Flowbite, shadcn-rails, or RailsUI?"
While those are great tools, they occupy different niches. Flowbite is fantastic for Tailwind-first projects but isn't built as a first-class ViewComponent library, often requiring you to wrap their HTML yourself. shadcn-rails follows the "copy-paste" philosophy which is great for total control, but lacks a clean, gem-based upgrade path for those who want their design system managed as a dependency. RailsUI is a premium, template-oriented solution that is excellent for rapid prototyping but might feel too opinionated for existing apps.
`jet_ui` sits in the "Goldilocks" zone: it's ViewComponent-native, Tailwind v4-native, and gem-distributed with an "eject-on-demand" safety valve. You get the maintenance benefits of a gem with the flexibility of local code when you need it.
Conclusion
Migration isn't just about "fancy" code. It's about reducing cognitive load. Developers can focus on building features using high-level components rather than wrestling with low-level utility classes in every single view. If you're building a modern Rails app, `jet_ui` is the bridge between the flexibility of Tailwind and the structure of a professional design system.