ODT UI for Ruby on Rails (odt-ui-rails)
Standalone CSS-first design tokens, Hotwire-ready UI components, and ActionView helpers for Ruby on Rails.
Overview
odt-ui-rails brings the ODT Design System directly into Ruby on Rails. It is architected from the ground up to provide a Standalone CSS First experience:
- 📦 Zero Host Build Tools Required: Ships pre-compiled CSS custom properties and
.odt-*component styles. Works out-of-the-box with Propshaft, Sprockets, or no asset pipeline at all. - âš¡ Seamless Tailwind CSS v4 Interoperability: Includes a complete
@themetoken configuration for Tailwind v4 projects, allowing custom utility classes (class: "...") to merge effortlessly onto components. - 🚀 Hotwire & Turbo Native: First-class helpers for Turbo Streams (
odt_toast_stream), Flash message auto-toasts (odt_flash_toasts), and Stimulus-powered Modals (odt_modal). - 💎 Strict Type Signatures: Full RBS definitions included for Ruby LSP, Steep, and Solargraph IDE autocompletion.
Table of Contents
Installation
Add the gem to your application's Gemfile:
gem "odt-ui-rails", "~> 2.2.4"
Install dependencies:
bundle install
Run the automated install generator:
bin/rails g odt:install
The generator will:
- Inject
<%= stylesheet_link_tag "odt_ui", "data-turbo-track": "reload" %>into yourapp/views/layouts/application.html.erb. - Configure Tailwind CSS v4
@theme(odt_theme.css) if Tailwind is detected. - Copy
sig/odt_ui.rbsfor Ruby LSP IDE autocompletion. - Copy Stimulus controllers (
odt_toast_controller.js,odt_modal_controller.js) toapp/javascript/controllers/if Stimulus is present.
Asset Pipeline Configuration
1. Application Layout (Propshaft / Sprockets / Standard Rails)
In app/views/layouts/application.html.erb, include the ODT stylesheet:
<head>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "odt_ui", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
2. Tailwind CSS v4 Theme Interop (app/assets/tailwind/application.css)
If using Tailwind CSS v4 and you want ODT design token utilities (bg-primary-500, text-fg-strong, etc.) in custom HTML:
@import "tailwindcss";
@import "./odt_theme.css";
(Note: odt_theme.css is placed in your tailwind assets folder by bin/rails g odt:install)
Component Reference
All helpers are globally available in all ERB templates, layouts, and helpers.
Feedback & Overlays
Toasts & Toaster
Provides viewport stacking containers, automated Flash message toasts on redirects, and Turbo Stream responses for async form actions.
<%# In app/views/layouts/application.html.erb %>
<body class="bg-surface text-fg font-sans antialiased">
<%= odt_toaster position: :top_right do %>
<%= odt_flash_toasts %>
<% end %>
<%= yield %>
</body>
Controller Turbo Stream Action:
def create
@todo = Todo.new(todo_params)
if @todo.save
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.prepend("todos", @todo),
odt_toast_stream("Todo created successfully!", variant: :success)
]
end
format.html { redirect_to todos_path, notice: "Todo created successfully!" }
end
end
end
Individual Toast Helper:
<%= odt_toast "Settings updated", title: "Saved", variant: :success, duration: 4000 %>
<%= odt_toast "Failed to sync records", variant: :danger %>
| Parameter | Type | Default | Description |
|---|---|---|---|
message |
String |
nil |
Toast description or body text |
title |
String |
nil |
Optional bold header title |
variant |
Symbol |
:success |
:success, :warning, :danger, :error, :info, :loading, :frosted |
duration |
Integer |
4000 |
Auto-dismiss duration in ms (0 to disable) |
dismissible |
Boolean |
true |
Renders close button (×) |
Modals
Accessible dialog containers with backdrop overlay, ESC key handling, scroll locking, and smooth scale transitions.
<%# Trigger Button %>
<%= odt_button "New Task", color: :primary, data: { action: "click->odt-modal#open" } %>
<%# Standard Modal %>
<%= odt_modal id: "new_task_modal", title: "Create Task", subtitle: "Add a task to your board", size: :md do %>
<%= render "form", todo: Todo.new %>
<% end %>
<%# Custom Composition Modal %>
<%= odt_modal id: "confirm_delete_modal", size: :sm do %>
<%= odt_modal_header title: "Delete Item?", subtitle: "This action cannot be undone." %>
<%= odt_modal_body do %>
<p class="text-sm text-fg-muted">Are you sure you want to permanently remove this record?</p>
<% end %>
<%= odt_modal_footer do %>
<%= odt_button "Cancel", variant: :ghost, data: { action: "click->odt-modal#close" } %>
<%= odt_button "Delete", color: :danger, data: { turbo_method: :delete } %>
<% end %>
<% end %>
| Parameter | Type | Default | Description |
|---|---|---|---|
id |
String |
nil |
HTML identifier for Stimulus/Turbo targeting |
title / subtitle |
String |
nil |
Header titles |
size |
Symbol |
:md |
:sm, :md, :lg, :xl, :full |
radius |
Symbol |
:"2xl" |
:none to :"4xl", :full |
dismissible |
Boolean |
true |
Renders close button |
open |
Boolean |
false |
Initial visible state |
prevent_backdrop_close |
Boolean |
false |
Disables closing on overlay click |
Alerts
Inline banners for status notices, warning messages, and form callouts.
<%= odt_alert "Your subscription will renew on May 1st.", variant: :info %>
<%= odt_alert "Payment failed. Please update billing info.", variant: :danger, icon: "fa-solid fa-triangle-exclamation" %>
Actions & Indicators
Buttons
Versatile action buttons supporting brand colors, frosted glass surfaces, loading states, and icon decorations.
<%# Standard Brand Action %>
<%= odt_button "Save Changes", color: :primary, variant: :filled, radius: :full %>
<%# Secondary & Frosted Glass %>
<%= odt_button "Secondary", variant: :subtle, color: :secondary %>
<%= odt_button "Frosted Glass", variant: :frosted, color: :primary %>
<%# Link as Button %>
<%= odt_button "View Project", href: project_path(@project), variant: :subtle %>
<%# Loading & Icons %>
<%= odt_button "Add Task", icon: "fa-solid fa-plus", color: :primary %>
<%= odt_button "Saving...", loading: true %>
<%= odt_button icon: "fa-solid fa-trash", icon_only: true, color: :danger, variant: :ghost %>
| Option | Type | Default | Description |
|---|---|---|---|
variant |
Symbol |
:filled |
:filled, :subtle, :ghost, :outline, :capsule, :frosted, :light |
color |
Symbol |
:primary |
:primary, :secondary, :surface, :muted, :inverse, :success, :warning, :danger, :info, :neutral |
size |
Symbol |
:md |
:xs, :sm, :md, :lg, :xl |
radius |
Symbol |
:"2xl" |
:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full |
icon / right_icon |
String |
nil |
FontAwesome class or inline SVG string |
icon_only |
Boolean |
false |
Square icon button formatting |
loading |
Boolean |
false |
Replaces icon with spinning loader |
disabled |
Boolean |
false |
HTML disabled state |
full_width |
Boolean |
false |
Expands width to 100% |
Badges
Compact status indicators with optional indicator dots and icons.
<%= odt_badge "Active", variant: :subtle, color: :success, dot: true %>
<%= odt_badge "High Priority", variant: :filled, color: :danger, radius: :full %>
<%= odt_badge "Calendar", icon: "fa-regular fa-calendar", variant: :outlined %>
Form Controls
Accessible form components complete with labels, helper text, error validations, icons, and input adornments.
<%= form_with model: @user, class: "space-y-4" do |f| %>
<%# Text Input with Left Icon %>
<%= odt_input(
label: "Email Address",
name: "user[email]",
value: @user.email,
type: "email",
placeholder: "you@example.com",
left_icon: "fa-solid fa-envelope",
error: @user.errors[:email].first,
required: true
) %>
<%# Textarea %>
<%= odt_textarea(
label: "Bio",
name: "user[bio]",
value: @user.bio,
rows: 4,
helper: "Brief summary for your profile"
) %>
<%# Select Dropdown %>
<%= odt_select(
label: "Country",
name: "user[country]",
options: [["Thailand", "TH"], ["United States", "US"], ["Japan", "JP"]],
selected: @user.country,
prompt: "Select country"
) %>
<%# Checkbox %>
<%= odt_checkbox(
label: "Subscribe to newsletter",
description: "Receive product updates once a week",
name: "user[subscribed]",
checked: true,
color: :primary
) %>
<%# Radio Group %>
<%= odt_radio label: "Monthly Billing", name: "plan", value: "monthly", checked: true %>
<%= odt_radio label: "Annual Billing (Save 20%)", name: "plan", value: "annual" %>
<%# Custom Slot Form Group %>
<%= odt_form_group label: "Custom Input", helper: "Unique value required" do %>
<input type="text" class="odt-input odt-input--outlined odt-input--md" name="custom" />
<% end %>
<% end %>
Layout & Typography
Cards
Versatile containers supporting header metadata, multiple surface styles, and hover transitions.
<%= odt_card title: "Analytics Overview", subtitle: "Monthly performance", variant: :elevated, radius: :"3xl" do %>
<p class="text-sm text-fg">Dashboard charts and statistics...</p>
<% end %>
<%= odt_card variant: :frosted, radius: :"2xl", hoverable: true do %>
<span>Interactive frosted glass card</span>
<% end %>
Typography (odt_heading & odt_text)
Standardized typographic elements mapped to the design system scale.
<%= odt_heading "Task Dashboard", as: :h1, size: :"4xl", weight: :bold, color: :strong %>
<%= odt_heading "Section Title", as: :h3, size: :xl %>
<%= odt_text "Manage all project deliverables in one workspace.", size: :sm, color: :muted %>
<%= odt_text "A very long single-line description...", truncate: true %>
Avatars
Renders image avatars or automatically computes two-letter initials from full names.
<%# Generates initials avatar "JD" %>
<%= odt_avatar name: "John Doe", size: :lg, color: :secondary %>
<%# Image avatar with fallback %>
<%= odt_avatar src: "https://avatar.vercel.sh/pixel", size: :md %>
Theming & Dark Mode
The design system is fully dark mode ready. Apply data-theme="dark" or the .dark class to <html> or any sub-container:
<html data-theme="dark" class="dark">
...
</html>
All design tokens (backgrounds, surfaces, borders, text, and shadows) adapt automatically without custom CSS.
License
The gem is available as open source under the terms of the MIT License.