Blocks

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.

How you appear to others.

We'll never share this.

Max 160 chars.

<%= 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.

Spans full width of the form.

<%= 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.

Text inputs

Numeric / date / time / color

Multi-line and file

A short description, max 160 chars.

PNG or JPG, up to 2 MB.

Select

Radios

FormBuilder emits bare radio inputs — wrap in sui.radio_group for the styled grouping.

Checkboxes

Switch (bare component)

Switch is a standalone component — not a FormBuilder method. Name it manually to submit.

<%= 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:)