UI components for Ruby on Rails, built with ERB, TailwindCSS 4.0, and Stimulus.
I started building components inspired by shadcn/ui for production Rails applications—mainly dashboards and admin interfaces. Over time, I iterated on these components across multiple projects, and they became inconsistent: different APIs, different styling approaches, different levels of completeness.
It was time to extract the elements I use most and give them a cohesive API and consistent styling.
I chose ERB partials with TailwindCSS and Stimulus controllers for interactive elements. For static components like form inputs, pure CSS with data attributes is enough.
I'm aware of alternatives like ViewComponent and Phlex. The projects I extracted these components from didn't use them. I see the benefits of using a Ruby class to render the UI, but bringing in any of these libraries into a project is a big commitment, and not all projects and teams are open to doing it. The reason is not technical; it is the feeling of moving away from "the Rails way." So I kept it simple: ERB partials that any Rails developer can understand immediately.
These components are built to be composable. They are built of many small ERB partials to render. But that's intentional—you can take these partials and compose them into larger, application-specific components. There are no limits, and you have a standard API to guide you.
I didn't copy shadcn/ui one-to-one. I extracted only the components I actually use in my applications. This is a practical toolkit, not a complete port.
There's no single UI kit that rules Rails development. If this approach doesn't resonate with you, here are excellent alternatives:
- RailsUI — Premium UI templates and components
- RailsBlocks — Copy-paste components for Rails
- shadcn-rails — Another shadcn/ui port for Rails
- Inertia Rails + shadcn Starter — React/Vue components with Inertia
If you're open to trying maquina_components and providing feedback, you're welcome to do so. If this isn't for you, that's okay too.
- ERB partials with strict locals (
locals:magic comments) - TailwindCSS 4.0 with CSS custom properties for theming
- Data attributes (
data-component,data-*-part) for CSS styling - Stimulus controllers only used where interactivity is needed
- Dark mode support via CSS variables
- shadcn/ui theming convention (works with their color system)
- Composable — small partials you can combine freely
# Gemfile
gem "maquina-components"bundle installbin/rails generate maquina_components:installThis will:
- Add the engine CSS import to your Tailwind file
- Add theme variables (light + dark mode)
- Create a helper file for icon customization
<%= render "components/card" do %>
<%= render "components/card/header" do %>
<%= render "components/card/title", text: "Account Settings" %>
<%= render "components/card/description", text: "Manage your preferences" %>
<% end %>
<%= render "components/card/content" do %>
<!-- Your content -->
<% end %>
<% end %>For form elements, use data attributes with Rails helpers:
<%= form_with model: @user do |f| %>
<%= f.text_field :name, data: { component: "input" } %>
<%= f.email_field :email, data: { component: "input" } %>
<%= f.submit "Save", data: { component: "button", variant: "primary" } %>
<% end %># Default: adds everything
bin/rails generate maquina_components:install
# Skip theme variables (if you have your own)
bin/rails generate maquina_components:install --skip-theme
# Skip helper file
bin/rails generate maquina_components:install --skip-helperPrerequisite: tailwindcss-rails must be installed first.
| Component | Description | Documentation |
|---|---|---|
| Sidebar | Collapsible navigation with cookie persistence | Sidebar |
| Header | Top navigation bar | Header |
| Component | Description | Documentation |
|---|---|---|
| Card | Content container with header, content, footer | Card |
| Alert | Callout messages (info, warning, error) | Alert |
| Badge | Status indicators and labels | Badge |
| Table | Data tables with sorting support | Table |
| Empty State | Placeholder for empty lists | Empty State |
| Component | Description | Documentation |
|---|---|---|
| Breadcrumbs | Navigation trail with overflow handling | Breadcrumbs |
| Dropdown Menu | Accessible dropdown with keyboard navigation | Dropdown Menu |
| Pagination | Page navigation with Pagy integration | Pagination |
| Component | Description | Documentation |
|---|---|---|
| Calendar | Inline date picker with single/range selection | Calendar |
| Combobox | Searchable dropdown with keyboard navigation | Combobox |
| Date Picker | Popover-based date selection | Date Picker |
| Toggle Group | Single/multiple selection button group | Toggle Group |
| Component | Description | Documentation |
|---|---|---|
| Toast | Non-intrusive notifications with auto-dismiss | Toast |
| Component | Data Attribute | Variants |
|---|---|---|
| Button | data-component="button" |
default, primary, secondary, destructive, outline, ghost, link |
| Input | data-component="input" |
— |
| Textarea | data-component="textarea" |
— |
| Select | data-component="select" |
— |
| Checkbox | data-component="checkbox" |
— |
| Radio | data-component="radio" |
— |
| Switch | data-component="switch" |
— |
<%= render "components/card" do %>
<%= render "components/card/header", layout: :row do %>
<div>
<%= render "components/card/title", text: "Team Members" %>
<%= render "components/card/description", text: "Manage your team" %>
</div>
<%= render "components/card/action" do %>
<%= link_to "Add Member", new_member_path,
data: { component: "button", variant: "primary", size: "sm" } %>
<% end %>
<% end %>
<%= render "components/card/content" do %>
<!-- Table or list -->
<% end %>
<% end %><%= render "components/alert", variant: :destructive do %>
<%= render "components/alert/title", text: "Error" %>
<%= render "components/alert/description" do %>
Your session has expired. Please log in again.
<% end %>
<% end %><%= render "components/badge", variant: :success do %>Active<% end %>
<%= render "components/badge", variant: :warning do %>Pending<% end %>
<%= render "components/badge", variant: :destructive do %>Failed<% end %><%= render "components/toggle_group", type: :single, variant: :outline do %>
<%= render "components/toggle_group/item", value: "left", aria_label: "Align left" do %>
<%= icon_for :align_left %>
<% end %>
<%= render "components/toggle_group/item", value: "center", aria_label: "Align center" do %>
<%= icon_for :align_center %>
<% end %>
<%= render "components/toggle_group/item", value: "right", aria_label: "Align right" do %>
<%= icon_for :align_right %>
<% end %>
<% end %><%= render "components/dropdown_menu" do %>
<%= render "components/dropdown_menu/trigger" do %>Options<% end %>
<%= render "components/dropdown_menu/content" do %>
<%= render "components/dropdown_menu/item", href: profile_path do %>
<%= icon_for :user %>
Profile
<% end %>
<%= render "components/dropdown_menu/separator" %>
<%= render "components/dropdown_menu/item", href: logout_path, method: :delete, variant: :destructive do %>
<%= icon_for :log_out %>
Logout
<% end %>
<% end %>
<% end %><%= pagination_nav(@pagy, :users_path) %><%= render "components/combobox", placeholder: "Select framework..." do |combobox_id| %>
<%= render "components/combobox/trigger", for_id: combobox_id, placeholder: "Select framework..." %>
<%= render "components/combobox/content", id: combobox_id do %>
<%= render "components/combobox/input", placeholder: "Search..." %>
<%= render "components/combobox/list" do %>
<%= render "components/combobox/option", value: "rails" do %>Ruby on Rails<% end %>
<%= render "components/combobox/option", value: "hanami" do %>Hanami<% end %>
<%= render "components/combobox/option", value: "sinatra" do %>Sinatra<% end %>
<% end %>
<%= render "components/combobox/empty" %>
<% end %>
<% end %><%# Add toaster to your layout %>
<%= render "components/toaster", position: :bottom_right do %>
<%= toast_flash_messages %>
<% end %>
<%# In your controller %>
flash[:success] = "Changes saved successfully!"
<%# Or use the JavaScript API %>
<script>
Toast.success("Profile updated!")
Toast.error("Something went wrong", { description: "Please try again." })
</script><%= render "components/table" do %>
<%= render "components/table/header" do %>
<%= render "components/table/row" do %>
<%= render "components/table/head" do %>Name<% end %>
<%= render "components/table/head" do %>Email<% end %>
<%= render "components/table/head" do %>Role<% end %>
<% end %>
<% end %>
<%= render "components/table/body" do %>
<% @users.each do |user| %>
<%= render "components/table/row" do %>
<%= render "components/table/cell" do %><%= user.name %><% end %>
<%= render "components/table/cell" do %><%= user.email %><% end %>
<%= render "components/table/cell" do %>
<%= render "components/badge", variant: :outline do %><%= user.role %><% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %><%= render "components/sidebar/provider", default_open: app_sidebar_open? do %>
<%= render "components/sidebar" do %>
<%= render "components/sidebar/header" do %>
<span class="font-semibold">My App</span>
<% end %>
<%= render "components/sidebar/content" do %>
<%= render "components/sidebar/group", title: "Navigation" do %>
<%= render "components/sidebar/menu" do %>
<%= render "components/sidebar/menu_item" do %>
<%= render "components/sidebar/menu_button",
title: "Dashboard",
url: dashboard_path,
icon_name: :home,
active: current_page?(dashboard_path) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= render "components/sidebar/inset" do %>
<%= render "components/header" do %>
<%= render "components/sidebar/trigger", icon_name: :panel_left %>
<% end %>
<main class="flex-1 p-6">
<%= yield %>
</main>
<% end %>
<% end %>Components use CSS variables following the shadcn/ui theming convention.
The install generator adds default theme variables. Customize them in app/assets/tailwind/application.css:
:root {
/* Change primary to blue */
--primary: oklch(0.488 0.243 264.376);
--primary-foreground: oklch(0.985 0 0);
/* Add custom colors */
--success: oklch(0.6 0.2 145);
--success-foreground: oklch(0.985 0 0);
}
@theme {
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
}| Helper | Purpose |
|---|---|
icon_for(name, options) |
Render an SVG icon |
sidebar_state(cookie_name) |
Get sidebar state (:expanded or :collapsed) |
sidebar_open?(cookie_name) |
Check if the sidebar is expanded |
pagination_nav(pagy, route) |
Render pagination from Pagy object |
pagination_simple(pagy, route) |
Render simple Previous/Next pagination |
toast_flash_messages |
Render all flash messages as toasts |
toast(variant, title, **options) |
Render a single toast notification |
combobox(placeholder:, **options, &block) |
Builder pattern for combobox |
combobox_simple(options:, **options) |
Data-driven simple combobox |
- Getting Started — Installation and setup
- Card — Content containers
- Alert — Callout messages
- Badge — Status indicators
- Table — Data tables
- Empty State — Empty state placeholders
- Breadcrumbs — Navigation trails
- Dropdown Menu — Dropdown menus
- Pagination — Page navigation
- Calendar — Inline date picker
- Combobox — Searchable dropdown selection
- Date Picker — Popover date selection
- Toggle Group — Toggle button groups
- Toast — Toast notifications
- Form Components — Buttons, inputs, and form styling
Run the dummy app:
cd test/dummy
bin/rails serverRun tests:
bin/rails testThis repository includes a Claude Code skill that teaches Claude how to build consistent, accessible UIs using maquina_components. The skill provides:
- Component catalog — Complete reference for all components with ERB examples
- Form patterns — Validation, error handling, and complex form structures
- Layout patterns — Sidebar navigation, page structure, responsive design
- Turbo integration — Turbo Frames, Streams, and component updates
- Spec checklist — Review criteria for UI implementation quality
Copy the skill/ directory to your Rails project:
cp -r /path/to/maquina_components/skill .claude/skills/maquina-ui-standardsSee the Skill README for detailed installation and usage instructions.
Bug reports and pull requests are welcome on GitHub at github.com/maquina-app/maquina_components.
Copyright (c) Mario Alberto Chávez Cárdenas
The gem is available as open source under the terms of the MIT License.
- Design patterns from shadcn/ui
- Built with TailwindCSS
- Powered by Ruby on Rails

