
Simple Form is a Rails gem that generates complete form markup — labels, hints, errors, and wrappers — from a single f.input call, replacing form_with’s manual field-by-field markup. Install it with bundle add simple_form, then rails generate simple_form:install. It works with Rails 7 and 8, with built-in Bootstrap support.
This guide helps you pick between form_with, Simple Form, and component-based form builders, then covers Simple Form in depth: installation, everyday usage, validation errors under Turbo, and custom inputs.
form_with vs Simple Form vs Form Components
Rails apps build forms in one of three ways today:
form_withships with Rails. You write every label, field, hint, and error message yourself, which gives you full control over the markup.- Simple Form builds on the Rails form builder. One
f.inputcall per attribute renders the label, the field with the right input type, the hint, and any validation error. - Component-based views with ViewComponent or Phlex move form markup into plain Ruby classes you can unit test. For Phlex, the Superform gem builds directly on
phlex-rails.
Here is how the three approaches compare:
form_with | Simple Form | ViewComponent / Phlex | |
|---|---|---|---|
| Ships with Rails | Yes | No, one gem | No, one or more gems |
| Markup per field | Manual label, field, and error markup | One f.input call generates everything | Whatever your component renders |
| Validation errors | You render object.errors yourself | Inline span.error per field, automatic | You decide, per component |
| CSS framework support | Manual | Bootstrap installer flag, configurable wrappers | Anything, hand-rolled |
| Best for | A few forms, custom designs | CRUD-heavy apps with uniform forms | Design systems, reusable form components |
A practical rule of thumb: default to form_with while your app has a handful of forms or a bespoke design. Reach for Simple Form once you maintain many uniform forms, such as admin panels and internal tools. Consider ViewComponent or Phlex when forms belong to a shared design system. ViewComponent is maintained by GitHub, is in long-term support as of version 4, and its docs report component unit tests running about 100x faster than the equivalent controller tests.
The rest of this guide covers the Simple Form path.
Does Simple Form Support Rails 8?
Yes. Simple Form 5.4.0 (October 2025) added official support for Rails 7.2, 8.0, and 8.1, and dropped Rails versions before 7. The current release, 5.4.1 (January 2026), adds Ruby 4.0 compatibility. The gemspec requires activemodel >= 7.0 and actionpack >= 7.0, so any Rails 7 or 8 app can use it.
Every snippet in this article ran against Simple Form 5.4.1 on Rails 8.1.3 and Ruby 3.4.10.
Installing Simple Form
Add the gem and run the install generator:
bundle add simple_form
bin/rails generate simple_form:installThe generator creates config/initializers/simple_form.rb, which holds the wrapper configuration: the HTML structure Simple Form builds around every input. The defaults work out of the box.
If your app uses Bootstrap, run the generator with the --bootstrap flag instead:
bin/rails generate simple_form:install --bootstrapThis adds a config/initializers/simple_form_bootstrap.rb initializer that makes Simple Form emit Bootstrap form classes like form-label, form-control, and invalid-feedback.
Basic Usage of Simple Form
Let’s build a signup form in a fresh Rails 8 app. First, generate a User model and migrate:
bin/rails g model User username:string password:string email:string remember_me:boolean
bin/rails db:migrateA production app should store a password_digest and use has_secure_password. A plain password column keeps this example focused on how Simple Form detects column types.
Next, a controller with new, create, and index actions:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
redirect_to users_path, notice: "Welcome, #{@user.username}!"
else
render :new, status: :unprocessable_entity
end
end
def index
@users = User.all
end
private
def user_params
params.expect(user: [:username, :password, :email, :remember_me])
end
end# config/routes.rb
Rails.application.routes.draw do
resources :users, only: [:new, :create, :index]
endThe form view is where Simple Form comes in. simple_form_for replaces form_with, and each field is one f.input call:
<!-- app/views/users/new.html.erb -->
<%= simple_form_for @user do |f| %>
<%= f.input :username, label: "Your username" %>
<%= f.input :password, hint: "No special characters." %>
<%= f.input :email, placeholder: "user@example.com" %>
<%= f.input :remember_me, inline_label: "Yes, remember me" %>
<%= f.button :submit %>
<% end %>Note what you did not write: no input types, no label tags, no wrapper divs. Simple Form picks the input type from each column, renders a <label> for every field, and wraps each pair in a classed <div> you can style.
Each of these options is optional. Without a label:, Simple Form humanizes the attribute name. The hint: renders a <span class="hint"> under the field, placeholder: sets the HTML attribute, and inline_label: puts a boolean’s label next to its checkbox instead of above it. You can also switch parts off per field: <%= f.input :password_confirmation, label: false %>.
Start the app with bin/rails s and open localhost:3000/users/new to try the form.
How Column Types Map to Form Fields
We never specified a type for any field. Simple Form maps each column type (and some attribute names) to an input type. Here are the most used mappings:
| Column Type | Generated HTML Element | Comment |
|---|---|---|
string | input[type=text] | |
(passwords) string | input[type=password] | Any column whose name contains “password” |
(emails) string | input[type=email] | Any column whose name contains “email” |
boolean | input[type=checkbox] | |
text | textarea | |
integer, float | input[type=number] | |
datetime | datetime select | |
time | time select |
The Simple Form README lists every available input type and default.
Booleans
Boolean attributes render as checkboxes by default. When you want radio buttons or a select instead, pass the as: option:
<%= f.input :remember_me, as: :radio_buttons %>That single line generates this HTML:
<div class="input radio_buttons optional user_remember_me">
<label class="radio_buttons optional">Remember me</label>
<input type="hidden" name="user[remember_me]" value="" />
<span class="radio">
<label for="user_remember_me_true">
<input
class="radio_buttons optional"
type="radio"
value="true"
name="user[remember_me]"
id="user_remember_me_true"
/>Yes
</label>
</span>
<span class="radio">
<label for="user_remember_me_false">
<input
class="radio_buttons optional"
readonly="readonly"
type="radio"
value="false"
name="user[remember_me]"
id="user_remember_me_false"
/>No
</label>
</span>
</div>One short Ruby line becomes a complete, labeled, accessible control group.
Customizing the Wrapper and Input HTML
As the previous example shows, Simple Form wraps each field in a <div> with predictable classes. You can customize both the wrapper and the input with the wrapper_html: and input_html: options:
<%= f.input :username, wrapper_html: { class: "signup-field" }, input_html: { maxlength: 20 } %>input_html: takes any HTML attribute: maxlength, value, id, data-* attributes for Stimulus controllers, and so on.
Displaying Validation Errors
Simple Form’s biggest everyday win is error display. Add a validation to the model:
# app/models/user.rb
class User < ApplicationRecord
validates :username, presence: true
endSimple Form reads that validation and marks the field as required with an asterisk (an <abbr title="required"> element) next to the label.
When a save fails, the create action from earlier re-renders the form:
if @user.save
redirect_to users_path, notice: "Welcome, #{@user.username}!"
else
render :new, status: :unprocessable_entity
endOn re-render, Simple Form attaches each error to its field automatically. The failed username field comes back as:
<div class="input string required user_username field_with_errors">
<label class="string required" for="user_username">
<abbr title="required">*</abbr> Your username
</label>
<input
class="string required"
aria-invalid="true"
type="text"
value=""
name="user[username]"
id="user_username"
/>
<span class="error">can't be blank</span>
</div>You get the field_with_errors wrapper class, aria-invalid on the input, and the message in a span.error, with no per-field error markup in your view.
Displaying Validation Errors with Turbo
The status: :unprocessable_entity argument in the create action is not optional. Since Rails 7, Turbo Drive intercepts form submissions. After a submission, Turbo expects either a redirect or an error status; a form re-rendered with a plain 200 OK is ignored. The symptom is a form that appears to do nothing: the user submits the form, the request succeeds in the log, and the page never changes.
Rendering with a 422 status tells Turbo to replace the page with the re-rendered form, errors included. Rails 8.1 scaffolds generate status: :unprocessable_content, the current name for HTTP 422, and the older :unprocessable_entity symbol still works. Use one of them on every failed create and update render.
In production, failed form submissions rarely look like they do in dev — they surface as silent 422s, ParameterMissing exceptions, and Turbo responses users never see. AppSignal’s Rails error tracking groups these by controller action, so you can see which forms actually fail for real users.
Custom Inputs and Additional Options
Simple Form ships with a full set of input types, and you can add your own. A custom input is a class in app/inputs inheriting from SimpleForm::Inputs::Base. Here is a social handle input that prefixes the field with an “@”:
# app/inputs/social_handle_input.rb
class SocialHandleInput < SimpleForm::Inputs::Base
def input(wrapper_options)
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
"@ #{@builder.text_field(attribute_name, merged_input_options)}".html_safe
end
endUse it by naming it in the as: option:
<%= f.input :network_handle, as: :social_handle %>If your User model has no network_handle column, add one through a migration or declare an attr_accessor in the model.
Associations get the same one-line treatment. Given a Team model and a belongs_to :team association on User, f.association renders a select with every team as an option:
<%= f.association :team %>Forms for Plain Ruby Objects
Simple Form is not limited to ActiveRecord models. Any class that includes ActiveModel::Model works as a form object:
# app/models/signup.rb
class Signup
include ActiveModel::Model
attr_accessor :company_name
end<%= simple_form_for Signup.new, url: "/users" do |f| %>
<%= f.input :company_name %>
<% end %>Pass url: explicitly, since a plain Ruby object has no resource route. This pattern suits multi-model forms and search forms that never touch a table.
i18n Support
Multi-language apps don’t need per-field options scattered through views. Simple Form follows the Rails i18n conventions: define labels, hints, and placeholders under a simple_form key in your locale files, and every form picks them up.
# config/locales/simple_form.en.yml
en:
simple_form:
labels:
user:
username: "User name"
hints:
user:
password: "No special characters, please."
placeholders:
user:
username: "Your username"The same structure works for prompts and include_blanks on select inputs. Options passed directly to f.input override the locale values.
Wrapping Up
You now have a decision rule and a working setup. form_with covers small apps and custom designs, component libraries cover design systems, and Simple Form covers the wide middle: apps full of uniform forms that should stay consistent without repeated markup. Let f.input infer your field types, and always render failed saves with a 422 status so Turbo shows your validation errors.
The Simple Form README documents the full wrapper API and every input type. And once your forms are live, AppSignal’s Rails integration tracks the errors and slow requests behind them in production.
Happy coding!
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- Does Simple Form support Rails 8?
- Yes. Simple Form 5.4 officially supports Rails 7.2, 8.0, and 8.1, and its gemspec requires ActiveModel and ActionPack 7.0 or newer. The current release, 5.4.1 from January 2026, also adds Ruby 4.0 compatibility.
- How do I install Simple Form in a Rails app?
- Run bundle add simple_form, then rails generate simple_form:install. The generator creates config/initializers/simple_form.rb with the default wrapper configuration. For Bootstrap projects, run the generator with the --bootstrap flag to get Bootstrap-ready wrappers instead.
- When should I use Simple Form instead of form_with?
- Use Simple Form when your app has many CRUD-style forms and you want labels, hints, and validation errors generated from one f.input call per field. Stick with form_with for a handful of forms, heavily customized designs, or zero extra dependencies.
- Why don’t my Rails form validation errors show up with Turbo?
- Turbo only renders a re-submitted form when the response carries a 422 status. Render your failed create or update with status: :unprocessable_entity, or :unprocessable_content on Rails 8.1. Without it, the page silently stays unchanged.
Published , Updated
Wondering what you can do next?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Thomas Riboulet
Our guest author Thomas is a Consultant Backend and Cloud Infrastructure Engineer based in France. For over 13 years, he has worked with startups and companies to scale their teams, products, and infrastructure. He has also been published several times in France's GNU/Linux magazine and on his blog.
All articles by Thomas RibouletBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


