rails-active-ui
A Fomantic-UI component system for Rails. Views use .html.ruby files with PascalCase component calls built on ActiveModel::Attributes.
Setup
Add to your Gemfile:
gem "rails-active-ui", path: "engines/rails-active-ui"
Requires Ruby >= 3.2 and Rails ~> 8.1.
Engine initializers
The gem's engine (Ui::Engine) registers the following automatically:
Autoload paths -- app/lib and app/blocks are added to the autoload paths so Component and block classes are available everywhere.
Asset paths -- formantic-ui/ (Fomantic-UI CSS/JS distribution) and app/javascript/ (Stimulus controllers) are added to Propshaft's asset paths. Reference them in your layout:
# CSS
StylesheetLink("stylesheets.css") # Fomantic-UI stylesheet
# jQuery + Fomantic-UI component JS (must come before importmap)
text
# Importmap (loads Stimulus controllers)
JavascriptImportmap()
Importmap -- the gem's config/importmap.rb is prepended to the app's importmap. It pins:
ui-- the main entry point (ui/index.js)ui/controllers/*-- all Fomantic-UI Stimulus bridge controllersemoji-picker-element-- emoji picker from CDN
Helpers -- ComponentHelper and FuiHelper are included into ActionView::Base automatically.
Stimulus controllers
Register the Fomantic-UI Stimulus controllers in your app's app/javascript/controllers/index.js:
import { Application } from "@hotwired/stimulus"
import { registerFuiControllers } from "ui"
const application = Application.start()
registerFuiControllers(application)
These are thin jQuery bridge controllers that initialize Fomantic-UI widgets in connect() and tear them down in disconnect(), making them Turbo-compatible. There are 28 Fomantic bridge controllers covering accordion, api, calendar, checkbox, datatable, dimmer, dropdown, embed, emoji-picker, flyout, form, item-list, modal, nag, popup, progress, rating, search, shape, sidebar, site, slider, state, sticky, tab, toast, transition, and visibility.
Rails engine usage
If you're using rails-active-ui inside a Rails engine, your engine needs to register the gem's assets manually since engines don't inherit the host app's asset paths:
# lib/my_engine/engine.rb
class Engine < ::Rails::Engine
initializer "my_engine.assets" do |app|
ui_gem = Gem::Specification.find_by_name("rails-active-ui")
app.config.assets.paths << File.join(ui_gem.gem_dir, "app/assets")
end
end
View DSL
Views use .html.ruby files with a pure-Ruby DSL. Every PascalCase call renders a component:
Header(size: :h2) { "Hello World" }
Segment(inverted: true) {
Paragraph { "Some content here" }
}
Text content
There are two ways to output text inside a block:
Block return value -- when the block contains only a string, just return it directly:
Header { "Page Title" } Button(color: "green") { "Save" }Textcomponent -- when you need to mix text with other components in the same block, useText { "..." }for each piece of text andNbSpace()for whitespace between components:Button(color: "blue") { Icon(name: "edit") NbSpace() Text { "Add Reply" } }
Prefer { "string" } over { Text { "string" } } when the block contains only text.
Never use lowercase text to emit inline content alongside components -- use Text { "..." } instead. Never embed leading/trailing spaces into text strings -- use NbSpace(). Every HTML element must be rendered through a component call.
Inline elements
Common HTML elements that appear inside components:
| HTML | Ruby DSL |
|---|---|
<a>Text</a> |
LinkTo { "Text" } |
<a class="user">Name</a> |
LinkTo(class: "user") { "Name" } |
<a href="/path">Link</a> |
LinkTo(href: "/path") { "Link" } |
<span class="ui red text"> |
Text(color: "red") { "..." } |
<div> (generic) |
Wrapper(class: "...") { ... } |
|
NbSpace() |
Utility helpers
| Helper | Output |
|---|---|
text "string" |
Appends plain text to the output buffer |
NbSpace() |
Appends |
Partial("path/to/partial") |
Delegates to render |
DocType() |
Outputs <!DOCTYPE html> |
StylesheetLink("file.css") |
Wraps stylesheet_link_tag |
JavascriptImportmap() |
Wraps javascript_importmap_tags |
CsrfMetaTags() |
Wraps csrf_meta_tags |
CspMetaTag() |
Wraps csp_meta_tag |
ContentFor(:name) { ... } |
Wraps content_for |
Style("css string") |
Renders an inline <style> tag |
Components
All components are registered in ComponentHelper::COMPONENT_MAP (app/helpers/component_helper.rb). Every PascalCase call in a .html.ruby view resolves through this map.
Base class
All components inherit from Component (app/lib/component.rb), which provides:
ActiveModel::Attributesfor declarative attribute definitions with types and defaults- Named slots via the
slotclass macro -- yieldsselfso callers fill regions:ruby Card { |c| c.header { "Title" } c.description { "Body text" } } - HTML pass-through --
id,class,data,style,role,tabindex,title,aria,target,relbypass attributes and merge into the rendered tag merge_html_options-- merges component-computed options with user-provided HTML options (classes concatenate, data hashes deep-merge)- Dev annotations -- in development, output is wrapped in
<!-- BEGIN ClassName -->/<!-- END ClassName -->comments
Component inventory
Layout Primitives: VStack, HStack, Column, Row, Pusher, Overlay, LinkTo, SubHeader
Globals: Reset, Site, Wrapper, Template, BackButton
Elements: Button, ButtonGroup, ButtonTo, Paragraph, Container, Divider, Emoji, Flag, Header, Icon, Image, Input, Tag, TagGroup, List, ListItem, ListContent, ListHeader, ListDescription, Loader, Placeholder, Rail, Reveal, Segment, SegmentGroup, Step, StepGroup, Text
Collections: Breadcrumb, Form, Grid, Menu, MenuItem, SubMenu, Message, Table, TableRow, TableCell
Views: Ad, ItemGroup, Card, Comment, CommentGroup, CommentReplyGroup, CommentReply, Feed, FeedItem, Item, Statistic
Modules: Accordion, AccordionItem, SubAccordion, Calendar, Dimmer, Dropdown, Embed, Flyout, Modal, Nag, Popup, Progress, Slider, Rating, Search, Shape, Sidebar, Sticky, Tab, TabGroup, Toast, Transition
Behaviors: Api, State, Visibility
Blocks: ResourceListBlock
Component patterns
Simple component -- attributes map to CSS class modifiers:
Button(color: "red", size: "large") { "Delete" }
# => <button class="ui red large button">Delete</button>
Component with slots -- child regions are filled via yielded self:
FeedItem { |e|
e.label { Image(src: "/images/avatar/small/elliot.jpg") }
e.summary {
LinkTo(class: "user") { "Elliot Fu" }
NbSpace()
Text { "added you as a friend" }
}
e.date_inline { "1 Hour Ago" }
e. {
LinkTo(class: "like") {
Icon(name: "like")
NbSpace()
Text { "4 Likes" }
}
}
}
CSS class passthrough -- for one-off Fomantic classes not covered by component attributes:
Menu(class: "equal width") { ... }
method_missing behavior
PascalCase calls not in COMPONENT_MAP are handled by method_missing:
- If inside a
Form()block, the call is tried as an underscored form builder method (e.g.TextField->f.text_field) - Otherwise, it falls through to
tag.public_send(tag_name), generating an arbitrary HTML element
This means typos in component names will not raise errors -- they silently generate unexpected HTML tags.
Form Builder
rails-active-ui ships with Ui::FomanticFormBuilder, a drop-in ActionView::Helpers::FormBuilder subclass that wraps every field helper in Fomantic-UI markup.
Set it as the default in your ApplicationController:
class ApplicationController < ActionController::Base
default_form_builder Ui::FomanticFormBuilder
end
Inside a Form() block, method_missing delegates to the form builder. Standard Rails form helpers become PascalCase calls:
Form(url: users_path, method: :post) {
TextField(:name, required: true)
EmailField(:email)
Select(:role, [["Admin", "admin"], ["User", "user"]], dropdown: true)
CheckBox(:terms, label: "I agree to the Terms")
HiddenField(:token)
Submit("Save", color: "green")
}
Available form helpers
.html.ruby call |
Form builder method | Description |
|---|---|---|
TextField(:name) |
f.text_field :name |
Text input wrapped in .field |
EmailField(:email) |
f.email_field :email |
Email input |
PasswordField(:password) |
f.password_field :password |
Password input |
NumberField(:age) |
f.number_field :age |
Number input |
UrlField(:website) |
f.url_field :website |
URL input |
TelephoneField(:phone) |
f.telephone_field :phone |
Telephone input |
SearchField(:q) |
f.search_field :q |
Search input |
ColorField(:color) |
f.color_field :color |
Color picker |
RangeField(:volume) |
f.range_field :volume |
Range slider |
DateField(:birthday) |
f.date_field :birthday |
Date picker (Fomantic calendar) |
DatetimeLocalField(:starts_at) |
f.datetime_local_field :starts_at |
Datetime picker |
TimeField(:alarm) |
f.time_field :alarm |
Time picker |
MonthField(:month) |
f.month_field :month |
Month picker |
WeekField(:week) |
f.week_field :week |
Week picker |
TextArea(:bio) |
f.text_area :bio |
Textarea |
EmojiField(:icon) |
f.emoji_field :icon |
Emoji picker (Stimulus-powered) |
Select(:role, choices) |
f.select :role, choices |
Select dropdown |
CheckBox(:terms) |
f.check_box :terms |
Checkbox with Fomantic styling |
RadioButton(:plan, "pro") |
f.radio_button :plan, "pro" |
Radio button |
HiddenField(:token) |
f.hidden_field :token |
Hidden input (no wrapper) |
FileField(:avatar) |
f.file_field :avatar |
File upload |
Submit("Save") |
f.submit "Save" |
Submit button with Fomantic styling |
Field options
All field helpers accept these options:
label:-- override label text (nilto suppress)required:-- adds "required" class and asteriskdisabled:-- adds "disabled" classreadonly:-- adds "read-only" classinline:-- label sits beside the inputwidth:-- Fomantic grid column word (e.g."six","three")error:-- error message string, adds "error" classwarning:-- warning message string, adds "warning" classhint:-- grey note beneath the inputfield_class:-- extra classes on the wrapping.fielddivinput_class:-- extra classes on the input element
Submit button options
color:-- Fomantic color (e.g."green","red","blue")size:-- Fomantic size (e.g."tiny","large")basic:-- basic button styleicon:-- icon name (e.g."checkmark")inverted:-- inverted style
Checkbox / Radio options
kind:--:checkbox(default),:slider, or:togglesize:-- Fomantic sizeinverted:-- inverted stylefitted:-- removes label paddingright_aligned:-- label appears on the left
Field groups
Form(url: users_path) {
FieldsGroup(equal_width: true) {
TextField(:first_name)
TextField(:last_name)
}
}
Form-level messages
Form(url: users_path) {
ErrorMessage("Something went wrong", ["Email is taken"])
SuccessMessage("All done!", "Profile updated.")
WarningMessage("Heads up", ["Verify your email"])
InfoMessage("Note", ["This is informational"])
}
FuiHelper
FuiHelper is auto-included into ActionView::Base and provides:
fui_javascript_tags-- emits<script>tags for jQuery and all 23 Fomantic-UI component scripts in the correct load order (site and transition first)datatable(columns:, options:, &block)-- renders a DataTables-powered table wrapped in afui-datatableStimulus controller div
Example Browser
The repository includes a full Rails application that serves as a live example browser for all components. Example views live under app/views/examples/ organized by Fomantic-UI category:
app/views/examples/
collections/ # breadcrumb, form, grid, menu, message, table
elements/ # button, container, divider, emoji, flag, header, icon, image, input, list, etc.
examples/ # attached, bootstrap, dashboard, fixed, grid, homepage, login, etc.
modules/ # accordion, calendar, checkbox, dimmer, dropdown, tab, etc.
views/ # advertisement, card, comment, feed, item, statistic
Run bin/rails server and visit http://localhost:3000 to browse all examples. See CONVERTING_EXAMPLES.md for the guide on converting Fomantic-UI HTML docs into .html.ruby example views.