typicms/bootforms
BootForms for Laravel simplifies building Bootstrap-styled forms with a fluent API, automatic validation error display, model binding, and sensible defaults. Generate clean form markup quickly while reducing repetitive HTML and boilerplate.
Install and wire it up
composer require typicms/bootforms
Package discovery should register the service provider. If you are on an older Laravel or have discovery disabled, add TypiCMS\BootForms\BootFormsServiceProvider to config/app.php manually. Check composer.json for the supported Laravel range before committing — this is a small, single-maintainer package (13 stars) maintained primarily for TypiCMS' own admin UI, so the version constraints track what TypiCMS itself needs.
You also need Bootstrap 5 CSS and JS loaded in the layout that renders the form. BootForms emits Bootstrap 5 class names (mb-3, form-control, form-select, form-check, is-invalid, etc.) but does not ship the CSS. Without it you get correct semantics and broken visuals, which is the most common "it's not working" report.
Where to look first
README.md — the API surface is small and fully documented there; read it end to end, it takes five minutes.src/FormBuilder.php / src/Form.php — the fluent builder and the renderer. This is where every field type and modifier lives, and it is the real documentation.resources/views/ — the Blade partials that actually render each control. If you need to change markup, this is your extension point.config/bootforms.php (publish with php artisan vendor:publish --tag=bootforms-config if present) — usually just error-display and layout defaults.First use case
Take one hand-written form in your app — an edit screen is ideal — and convert it. The shape is:
{!! BootForm::open()->action(route('posts.update', $post))->put() !!}
{!! BootForm::text('title', 'Post title') !!}
{!! BootForm::textarea('body') !!}
{!! BootForm::select('status', 'Status', ['draft' => 'Draft', 'live' => 'Live']) !!}
{!! BootForm::submit('Save') !!}
{!! BootForm::close() !!}
Then delete the equivalent markup from the Blade file. If the form looks right and old input repopulates on a validation failure, the package is doing its job.
Open/close with explicit verbs. Use ->action() plus ->method() / ->put() / ->patch() / ->delete() so the builder emits the hidden _method field for you. Don't hand-write @method next to a BootForm opener — you'll get two.
{!! BootForm::open()->action('/posts')->post() !!}
{!! BootForm::open()->action(route('posts.update', $post))->put()->multipart() !!}
->multipart() is the one people forget; without it file uploads silently arrive empty.
Model binding beats manual ->value(). Bind the model once and let the builder read attributes and old input:
{!! BootForm::open()->action(...)->put()->model($post) !!}
{!! BootForm::text('title') !!}
{!! BootForm::close() !!}
Then only reach for ->value() on fields that aren't model attributes (search filters, computed defaults). Mixing the two — binding a model and setting ->value() everywhere — makes it ambiguous which wins when old input is present.
Layout modes are set on the opener, not per field. ->horizontal() (with label-width options) and ->inline() apply to the whole form. Pick one per form; switching mid-form produces misaligned columns that are tedious to debug.
Modifiers chain, and they're the whole ergonomic win:
{!! BootForm::text('slug')
->placeholder('auto-generated')
->help('Leave blank to derive from the title.')
->required()
->readonly($post->exists) !!}
->required(), ->disabled(), ->readonly(), ->help(), ->placeholder(), ->label() and ->value() all return the field, so order rarely matters. ->required() sets the HTML attribute only — it is not a validation rule. Always pair it with a FormRequest rule; BootForms renders errors but does not enforce anything.
Errors come from the shared error bag. The builder reads $errors for the field name and applies is-invalid / renders invalid-feedback. Two consequences:
@if($errors->any()) alert block if you want a summary; the per-field messages appear automatically.FormRequest::errorBag) need the bag wired through the builder or the field stays clean. If you use them, verify against src/Form.php — this is the area most likely to need a one-line customisation.Selects, checkboxes and radios. Select options take an associative array and support ->select($value) for the default. Checkbox and radio groups take name, label, options and render Bootstrap 5 form-check markup. For a single boolean, BootForm::checkbox('published', 'Published') in the classic API is a lone toggle, not a group — check which signature your installed version uses in src/FormBuilder.php before assuming.
Reuse via partials or Blade components. Wrap common field clusters (address block, SEO metadata block) in resources/views/partials/fields/*.blade.php and @include them. Pass the builder in if you need it, but the simpler pattern is to let each partial call BootForm:: statically — it's a facade-style singleton, so no wiring is required.
Custom field types: use macros. Rather than forking, register a macro in a service provider if the package supports it (FormBuilder::macro(...)); otherwise extend the builder class and swap the container binding. For a package this size, prefer macros and custom Blade partials over a fork — you want to stay on upstream releases.
Everything is raw output. You must render with {!! !!}, not {{ }}. The builder escapes attribute values, but the returned HTML is trusted markup — never pass unescaped user input into label, help, or option text expecting another round of escaping.
Bootstrap 5 renamed the form classes. If you're porting templates from the original BootForms (Bootstrap 3) or from Bootstrap 4 projects: form-group is gone (use mb-3), form-control-file and custom-control custom-checkbox are gone (use form-check), and .row/.col-form-label grids behave differently. Any old CSS or JS you wrote against custom-control will quietly stop applying. This rename is the single biggest source of "the classes look almost right" bugs.
Old input vs. model binding precedence. After a failed validation, old input wins over the model attribute — that's what you want, but it surprises people who set ->value() explicitly and find it ignored on redisplay. If a value must never be repopulated (e.g. a password field), confirm the builder treats password inputs specially or zero it out server-side.
File inputs cannot be repopulated. Browsers forbid it. Don't design a flow that depends on the filename surviving a validation round-trip.
Arrays in select/checkbox fields need explicit value casting. For multiple selects, old input arrives as an array; if your default is an Eloquent collection, cast to an array before passing it as the default value or nothing appears selected.
Validation is entirely your job. BootForms is a renderer. You still need a FormRequest, rules, and authorisation. The ->required() attribute is a hint to the browser, not a guard.
Debugging rendered markup. Instead of reading a 200-line Blade output in devtools, dump the generated string: call the builder, dd((string) $field) or echo it into a <pre>. When a class or an is-invalid is missing, the answer is almost always (a) the field name doesn't match the error key, or (b) $errors isn't shared into the view — check HandleExceptions/ShareErrorsFromSession middleware is active for that route.
Small package, thin release history. With 13 stars and a single organisational maintainer, expect the README to be the only documentation and expect issues to go unanswered. Pin an exact version in composer.json, keep a short internal note on which BootForms version maps to which Bootstrap 5 minor, and treat major upgrades as a manual review of src/Form.php rather than a composer update you trust. If you need a form builder with a larger community, Laravel Collective's HTML package or plain Blade components are the alternatives to weigh — BootForms' advantage is genuinely the concise chainable syntax and Bootstrap 5 markup done for you.
Extension points worth knowing: published views (change markup globally without touching call sites), builder macros (add project-specific field types), and the service provider binding (swap in a subclass for app-wide behaviour). Prefer them in that order.
How can I help you explore Laravel packages today?