FormBuilder
A Rails-native FormBuilder subclass that injects shadcnrb classes into every field. Pass builder: Shadcnrb::FormBuilder::Component to form_with.
Installation
bin/rails g shadcnrb:component form_builder
Thin API — classic Rails
Every Rails field helper still works. The builder just injects the matching
shadcnrb component's classes, so f.text_field :email renders
the same input as sui.input. Labels, inputs, selects stay
as separate calls — you compose the layout.
<%= form_with scope: :user, url: "#", builder: Shadcnrb::FormBuilder::Component, class: "max-w-md space-y-4" do |f| %> <%= sui.layout.stack gap: 2 do %> <%= f.label :name %> <%= f.text_field :name %> <% end %> <%= sui.layout.stack gap: 2 do %> <%= f.label :email %> <%= f.email_field :email %> <% end %> <%= sui.layout.stack gap: 2 do %> <%= f.label :bio %> <%= f.text_area :bio, rows: 3 %> <% end %> <%= sui.layout.stack gap: 2 do %> <%= f.label :plan %> <%= f.select :plan, [["Hobby","hobby"],["Pro","pro"],["Team","team"]] %> <% end %> <%= sui.layout.row gap: 2 do %> <%= f.check_box :terms %> <%= f.label :terms, "Accept terms" %> <% end %> <%= f.submit "Save" %> <% end %>
Bundled shortcut — f.field
When you want the label + control + hint/error layout without writing the stack manually,
use f.field. Uses the form_field component and
automatically pulls error messages from the model.
<%= form_with scope: :user, url: "#", builder: Shadcnrb::FormBuilder::Component, class: "max-w-md space-y-4" do |f| %> <%= f.field :name, label: "Display name", hint: "How you appear to others." %> <%= f.field :email, label: "Email", as: :email_field, hint: "We'll never share this." %> <%= f.field :bio, label: "Bio", as: :text_area, hint: "Max 160 chars." %> <%= f.submit "Save" %> <% end %>
Controlling width
Inputs are w-full by default, so field width is controlled by
the parent. Pass class: on the form (or a
sui.layout.container wrapper) to set a max-width, or use
sui.layout.grid to arrange fields side-by-side.
<%= form_with scope: :user, url: "#", builder: Shadcnrb::FormBuilder::Component, class: "max-w-2xl space-y-4" do |f| %> <%= sui.layout.grid cols: 1, gap: 4, class: "md:grid-cols-2" do %> <%= f.field :first_name, label: "First name" %> <%= f.field :last_name, label: "Last name" %> <% end %> <%= f.field :email, label: "Email", as: :email_field, hint: "Spans full width of the form." %> <%= sui.layout.grid cols: 1, gap: 4, class: "md:grid-cols-3" do %> <%= f.field :city, label: "City" %> <%= f.field :region, label: "Region" %> <%= f.field :postcode, label: "Postcode" %> <% end %> <%= f.submit "Save" %> <% end %>
For a single field, wrap it in any layout primitive with a width class —
sui.layout.stack(class: "max-w-xs"), class: "w-96", etc.
<%= sui.layout.stack gap: 2, class: "max-w-xs" do %> <%= sui.label "Tight field", for: "tight" %> <%= sui.input id: "tight", placeholder: "max-w-xs" %> <% end %>
Kitchen sink — every form input
Every field type wired through Shadcnrb::FormBuilder::Component, with some
bare component calls (sui.switch, sui.radio_group)
mixed in where the FormBuilder doesn't have a direct equivalent.
<%= form_with scope: :profile, url: "#", builder: Shadcnrb::FormBuilder::Component, class: "max-w-2xl space-y-6" do |f| %> <%= sui.h3 "Text inputs" %> <%= sui.layout.grid cols: 1, gap: 4, class: "md:grid-cols-2" do %> <%= f.field :name, label: "Name", as: :text_field %> <%= f.field :email, label: "Email", as: :email_field %> <%= f.field :password, label: "Password", as: :password_field %> <%= f.field :phone, label: "Phone", as: :telephone_field %> <%= f.field :url, label: "Website", as: :url_field %> <%= f.field :search, label: "Search", as: :search_field %> <% end %> <%= sui.separator %> <%= sui.h3 "Numeric / date / time / color" %> <%= sui.layout.grid cols: 1, gap: 4, class: "md:grid-cols-3" do %> <%= f.field :age, label: "Age", as: :number_field %> <%= f.field :birthday, label: "Birthday", as: :date_field %> <%= f.field :meeting, label: "Meeting", as: :datetime_local_field %> <%= f.field :alarm, label: "Alarm", as: :time_field %> <%= f.field :theme, label: "Color", as: :color_field %> <%= f.field :volume, label: "Volume", as: :range_field %> <% end %> <%= sui.separator %> <%= sui.h3 "Multi-line and file" %> <%= f.field :bio, label: "Bio", as: :text_area, hint: "A short description, max 160 chars." %> <%= f.field :file, label: "Avatar", as: :file_field, hint: "PNG or JPG, up to 2 MB." %> <%= sui.separator %> <%= sui.h3 "Select" %> <%= sui.layout.stack gap: 2, class: "max-w-xs" do %> <%= f.label :plan %> <%= f.select :plan, [["Hobby","hobby"],["Pro","pro"],["Team","team"]] %> <% end %> <%= sui.separator %> <%= sui.h3 "Radios" %> <%= sui.muted { "FormBuilder emits bare radio inputs — wrap in sui.radio_group for the styled grouping." } %> <%= sui.radio_group do %> <% %w[email sms push none].each do |v| %> <%= sui.layout.row gap: 2 do %> <%= f.radio_button :contact, v, id: "contact-#{v}" %> <%= f.label :contact, v.titleize, value: v, for: "contact-#{v}" %> <% end %> <% end %> <% end %> <%= sui.separator %> <%= sui.h3 "Checkboxes" %> <%= sui.layout.stack gap: 2 do %> <% [[:terms, "Accept the terms"], [:newsletter, "Send me occasional updates"], [:beta, "Opt in to beta features"]].each do |(field, label)| %> <%= sui.layout.row gap: 2 do %> <%= f.check_box field %> <%= f.label field, label %> <% end %> <% end %> <% end %> <%= sui.separator %> <%= sui.h3 "Switch (bare component)" %> <%= sui.muted { "Switch is a standalone component — not a FormBuilder method. Name it manually to submit." } %> <%= sui.layout.row gap: 2 do %> <%= sui.switch id: "2fa", name: "profile[two_factor]" %> <%= sui.label "Enable two-factor auth", for: "2fa" %> <% end %> <%= sui.separator %> <%= sui.layout.row gap: 2, justify: :end do %> <%= sui.button "Cancel", variant: :outline %> <%= f.submit "Save profile" %> <% end %> <% end %>
Methods overridden
Each injects the matching component's class string. You can still pass
class: to merge additional Tailwind.
- Text-like: text_field, email_field, password_field, number_field, date_field, datetime_field, datetime_local_field, time_field, month_field, week_field, url_field, tel_field, phone_field, search_field, color_field, file_field, range_field
- Multi-line / choice: text_area, select, check_box, radio_button
- Copy: label (with automatic humanization)
- Submit / action: submit, button
- Bundled: field(method, label:, hint:, as:)