Blocks

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.

How you appear to others.

We'll never share this.

Max 160 chars.

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.

Spans full width of the form.

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.

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.

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