Skip to main content

Components

UI components for Rails. ERB partials styled with Tailwind CSS 4.0 and Stimulus controllers. Inspired by shadcn/ui, built for Rails.

Production-ready UI components for Rails applications. Copy-paste ERB partials styled with Tailwind CSS 4.0 and optional Stimulus controllers.

What you get:

  • 20+ components — From layouts to forms, navigation to feedback
  • Zero dependencies — Just Tailwind CSS and optionally Stimulus
  • Token-driven theming — Familiar shadcn/ui CSS variables for color, plus tokens for shape, elevation, focus rings and weight
  • Rails conventions — ERB partials, data attributes, form helpers
Maquina Components light theme Maquina Components dark theme

Already on 0.5.1? 0.6.0 is a deliberately breaking release: engine CSS moved into @layer components, and radius, elevation, focus rings and weights became tokens. One change affects every existing app and fails silently — the unlayered * rule in your installed theme.css. Run the scanner, then read the guide:

bundle update maquina-components
bin/rails maquina:doctor

Upgrading to 0.6.0 · Theming

Latest release: 0.7.1. No API changes, but not purely additive: a required field with no placeholder used to paint the error state from first paint, and the fix means an app that renders server-side errors without setting aria-invalid loses its error border. Field error text gains its own --destructive-text token — one line of theme CSS if your palette is the saturated kind. The dropdown menu and menu button now flip when they hit the bottom of the viewport, and all eighteen leaf partials accept a block. maquina:doctor gained five rules for this release.

What changes in 0.7.1 · Theming


Demo Application

View Live Demo →

Explore all components in action without installing anything. The demo showcases light/dark themes, color themes, and responsive layouts.

For local development, clone the components repository:

git clone https://github.com/maquina-app/maquina_components.git
cd maquina_components/test/dummy
bin/rails server

Visit http://localhost:3000 to explore the components locally.


Quick Start

1. Add the Gem

# Gemfile
gem "maquina-components"
bundle install

2. Run the Install Generator

bin/rails generate maquina_components:install

This adds the engine CSS import, theme variables (shadcn/ui convention), a shape/state token block for radius, elevation, focus rings and weights, and a helper file for icon customization.

Re-running the generator is safe: it is idempotent, appends each block only once, and never rewrites your palette.

3. Start Using Components

<%= render "components/card" do %>
  <%= render "components/card/header" do %>
    <%= render "components/card/title", text: "Welcome" %>
  <% end %>
  <%= render "components/card/content" do %>
    <p>Your content here</p>
  <% end %>
<% end %>

Every partial takes its content three ways, and they are interchangeable — containers and leaves alike:

<%= render "components/card/title", text: "Welcome" %>
<%= render "components/card/title", content: "Welcome" %>
<%= render "components/card/title" do %>Welcome<% end %>

text: wins when present, then content:, then the block. Pass a block whenever the content is markup rather than a string. Before 0.7.1 half the leaf partials silently dropped the block, so if you worked around that with capture, you no longer need to.

For form elements, use data attributes:

<%= form_with model: @user do |f| %>
  <%= f.text_field :email, data: { component: "input" } %>
  <%= f.submit "Save", data: { component: "button", variant: "primary" } %>
<% end %>

AI-Assisted Development

Use the Maquina UI Standards Claude Code plugin to generate views that follow component conventions automatically.

Instead of correcting AI-generated code (“use the card partial, not a div”), the plugin teaches Claude your component patterns:

> Create the users index view with a table showing name, email, and status

Claude generates code using your actual components — proper partials, correct data attributes, and consistent patterns.


Available Components

Layout

Component Description
Sidebar Collapsible navigation with mobile support and keyboard shortcuts
Header Page header for sidebar layouts with breadcrumbs and actions
Drawer Slide-out panel with overlay, persistence, and keyboard shortcut

Content

Component Description
Card Content containers with header, body, and footer sections
Alert Callouts with 4 variants and icon support
Badge Status indicators with 7 variants and 3 sizes
Table Responsive data tables with striped and bordered variants, sticky headers, and a collection helper
Empty State Placeholder for no-data scenarios with icons and actions
Separator Horizontal or vertical divider
Stats Metric cards in a responsive grid
Component Description
Breadcrumbs Navigation with responsive collapsing support
Dropdown Menu Actions menu triggered by a button with keyboard navigation
Pagination Navigation for paginated content with Pagy integration

Interactive

Component Description
Calendar Date selection with single and range modes
Combobox Searchable dropdown with keyboard navigation and filtering
Date Picker Popover calendar triggered by a button for date selection
Toggle Group Single or multiple selection button groups

Feedback

Component Description
Toast Non-intrusive notifications with auto-dismiss and variants

Forms

Component Description
Form Components Inputs, selects, checkboxes styled with data attributes

Prerequisites

The generator requires tailwindcss-rails:

bundle add tailwindcss-rails
bin/rails tailwindcss:install

Stimulus Setup

Interactive components (Sidebar, Dropdown Menu, Toggle Group, Breadcrumbs, Combobox, Toast) require Stimulus. With importmaps:

# config/importmap.rb
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
// app/javascript/application.js
import "@hotwired/turbo-rails"
import { Application } from "@hotwired/stimulus"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"

const application = Application.start()
application.debug = false
window.Stimulus = application

eagerLoadControllersFrom("controllers", application)

Static components (Badge, Card, Alert, Button, form elements) work without JavaScript.

Extending Component Behavior

Every component merges your data: hash with its own data attributes. Identity keys (component, variant, size) always win, but controller and action concatenate — so you can attach your own Stimulus behavior to any component without losing the built-in one:

<%= render "components/combobox", name: "country",
      data: { controller: "analytics", action: "change->analytics#track" } %>
<%# renders data-controller="combobox analytics" %>

Icons

Components render icons through the icon_for helper, which falls back to a built-in set of inline SVGs (check, chevrons, calendar, search, mail, trash, and more):

<%= icon_for :check, class: "size-4" %>
<%= icon_for :trash, class: "size-4", stroke_width: 1.5 %>

To use your own icon system (Heroicons, Lucide, inline SVG files), override main_icon_svg_for in the generated MaquinaComponentsHelpericon_for consults it first and only falls back to the built-ins when it returns nil:

# app/helpers/maquina_components_helper.rb
def main_icon_svg_for(name)
  lucide_icon(name)
end

Or return the SVG yourself, a name at a time:

module MaquinaComponentsHelper
  def main_icon_svg_for(name)
    case name
    when :home
      <<~SVG
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/>
          <path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
        </svg>
      SVG
    end
  end
end

Icons are sourced from Lucide. Copy SVG code directly from their website.

What the override does and does not reach

main_icon_svg_for backs the public icon_for helper — every icon you render, plus the component parameters that take an icon name (alert, sidebar menu items, empty states, breadcrumb separators).

It deliberately does not reach the icons an engine component renders for itself: a dropdown trigger’s chevron, the toast close button, the calendar’s arrows. Those go through an internal builtin_icon_for that only ever reads the engine’s own set, so a component looks the same in every app regardless of how you have configured icons — and so a partial override cannot leave a control without its affordance.

The practical consequence: if an engine component’s own icon looks wrong or missing, defining that name in main_icon_svg_for will not change it. That is a bug in the engine, not something to fix in your app — please report it.

Catching typos: strict_icons

An unknown icon name renders nothing at all, which is invisible in review and in production. MaquinaComponents.strict_icons raises UnknownIconError instead. It is on by default in development and test and off in production, so a typo fails loudly while you work and can never take a page down for a user.

# config/initializers/maquina_components.rb
MaquinaComponents.strict_icons = false   # opt out; unknown names render nothing

This covers both helpers. If it raises for a name you never wrote yourself, an engine component asked for an icon the engine does not ship — the message says so, and says that a main_icon_svg_for entry will not help.


Theme Variables

Colors are CSS variables following the shadcn/ui theming convention.

Each one is defined twice, which Tailwind CSS v4 requires: in :root for the value, and in @theme so it also becomes a utility (bg-primary, text-muted-foreground).

:root {
  --primary: oklch(0.488 0.243 264.376);
  --primary-foreground: oklch(0.985 0 0);
}

@theme {
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
}

Edit the values in :root to match your brand — the generator installs neutral grays. Adding a semantic color works the same way: declare it in :root, then mirror it in @theme if you want the utility.

Everything that is not a color — shape, focus rings, elevation, weights, control marks — is a token too, and a theme changes values, not selectors. See Theming for the full token table, ready-made themes, and how to pin a single component.

Generator Options

# Skip theme variables (if you already have them)
bin/rails generate maquina_components:install --skip-theme

# Skip helper creation
bin/rails generate maquina_components:install --skip-helper

# Skip both
bin/rails generate maquina_components:install --skip-theme --skip-helper

File Structure After Setup

app/
├── assets/tailwind/
│   └── application.css               # Theme + engine import
├── helpers/
│   └── maquina_components_helper.rb  # Icon override
├── javascript/
│   └── application.js                # Stimulus init
└── views/layouts/
    └── application.html.erb          # Layout with components

Troubleshooting

Generator Issues

“tailwindcss-rails doesn’t appear to be installed”

Install it first:

bundle add tailwindcss-rails
bin/rails tailwindcss:install

Runtime Issues

Sidebar trigger not working

  • Ensure Stimulus is initialized
  • Verify provider wraps both sidebar and content
  • Check browser console for errors

Styles not applying

  • Verify engine CSS is imported after @import "tailwindcss";
  • Check that @theme block exists with color bindings
  • Restart dev server after CSS changes

Dark mode not working

  • Add .dark class to <html> element
  • Ensure .dark { } block has variable overrides

Icons not rendering

  • Check the icon name matches your main_icon_svg_for cases
  • Verify the helper is included in ApplicationHelper
  • Turn on strict_icons in development so an unknown name raises instead of rendering nothing
  • If the missing icon belongs to a component rather than to your own markup (a trigger’s chevron, a close button), main_icon_svg_for cannot fix it — see Icons

Next Steps