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.

Gem Version License: MIT Ruby Rails Hotwire Ready Type Checked


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 @theme token 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.3"

Install dependencies:

bundle install

Run the automated install generator:

bin/rails g odt:install

The generator will:

  1. Inject <%= stylesheet_link_tag "odt_ui", "data-turbo-track": "reload" %> into your app/views/layouts/application.html.erb.
  2. Configure Tailwind CSS v4 @theme (odt_theme.css) if Tailwind is detected.
  3. Copy sig/odt_ui.rbs for Ruby LSP IDE autocompletion.
  4. Copy Stimulus controllers (odt_toast_controller.js, odt_modal_controller.js) to app/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.