FormBuilder
A Rails-native FormBuilder subclass that injects shadcnrb classes into every field. Pass builder: Shadcnrb::FormBuilder to form_with.
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.
live_example do form_with scope: :user, url: "#", builder: Shadcnrb::FormBuilder, 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 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.
live_example do form_with scope: :user, url: "#", builder: Shadcnrb::FormBuilder, 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 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.
live_example do form_with scope: :user, url: "#", builder: Shadcnrb::FormBuilder, 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 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.
live_example do 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 end
Kitchen sink — every form input
Every field type wired through Shadcnrb::FormBuilder, with some
bare component calls (sui.switch, sui.radio_group)
mixed in where the FormBuilder doesn't have a direct equivalent.
live_example do form_with scope: :profile, url: "#", builder: Shadcnrb::FormBuilder, 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:)