Upgrading an existing application? Start with Upgrading from v3.
Staying on v2 or v3? Their documentation is archived here.
Bootstrap Form
This package simplifies Bootstrap 4 & 5 forms creation in Laravel applications 12+
It renders Bootstrap 5 markup by default and fully supports Bootstrap 4 for backward compatibility.
Model binding and automatic error display are supported, as well as most Bootstrap form features: form layouts, custom fields, input groups, and more.
An LLM-optimized documentation is included for your AI assistants.
Any contribution or feedback is highly welcomed, please feel free to create a pull request or submit a new issue.
Installation
Requirements
- PHP 8.2 or higher
- Laravel 12 or 13
Install
Install the package using Composer:
composer require bgaze/bootstrap-form
Several configuration options are available; publish the configuration file to customize them:
php artisan vendor:publish --provider="Bgaze\BootstrapForm\BootstrapFormServiceProvider"
That's it, you can start building forms.
Nothing else is required: the BF facade, the Blade directives and the bf x-components are all registered by the service provider.
One more line is worth adding if you build forms with an AI assistant: see AI coding assistants.
Upgrading from v3
v4 introduces breaking changes. Four of them matter when upgrading an existing application:
| Change | What it means for your application |
|---|---|
| Bootstrap 5 is now the default | A v3 application that never set bootstrap_version was rendering Bootstrap 4. To keep that markup untouched, set 'bootstrap_version' => 4 in the configuration file. |
No more laravelcollective/html |
The dependency is gone, replaced by an internal HTML/form layer that renders the same markup. BF::htmlBuilder() and BF::formBuilder() no longer exist. |
| PHP 8.2+, Laravel 12 or 13 | Older versions are no longer supported. |
| New configuration keys | Republish the configuration file with --force, or add the new keys by hand. Any key you do not declare falls back to its packaged default, so a partial file keeps working. |
Everything else is additive: the facade methods and Blade directives of v3 all still exist, with the same signatures.
Using an earlier version
To keep using a previous major version, require it explicitly:
| Version | Install | Documentation |
|---|---|---|
| v3 — Bootstrap 4 by default, Bootstrap 5 opt-in | composer require "bgaze/bootstrap-form:^3.0" |
Documentation (archived) |
| v2 — Bootstrap 4 only | composer require "bgaze/bootstrap-form:^2.0" |
Documentation (archived) |
Quick start
A form is opened, filled with fields, then closed.
The three syntaxes below are interchangeable and render exactly the same HTML.
{{-- x-components — the default in Blade templates --}}
<x-bf::form url="/login">
<x-bf::text name="login"/>
<x-bf::password name="password"/>
<x-bf::checkbox name="remember" label="Remember me" switch/>
<x-bf::submit>Log in</x-bf::submit>
</x-bf::form>
{{-- Blade directives — the historical syntax, still fully supported --}}
@open(['url' => '/login'])
@text('login')
@password('password')
@checkbox('remember', 'Remember me', 1, null, ['switch' => true])
@submit('Log in')
@close
// The BF facade — in PHP code
echo BF::open(['url' => '/login']);
echo BF::text('login');
echo BF::password('password');
echo BF::checkbox('remember', 'Remember me', 1, null, ['switch' => true]);
echo BF::submit('Log in');
echo BF::close();
<form method="POST" action="https://example.com/login" accept-charset="UTF-8" role="form">
<input name="_token" type="hidden">
<div id="login-group" class="mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control" name="login" type="text">
</div>
</div>
<div id="password-group" class="mb-3">
<label for="password" class="form-label">Password</label>
<div>
<input id="password" class="form-control" name="password" type="password" value="">
</div>
</div>
<div id="remember-group" class="mb-3">
<div>
<div class="form-check form-switch">
<input id="remember" class="form-check-input" role="switch" name="remember" type="checkbox" value="1">
<label for="remember" class="form-check-label">Remember me</label>
</div>
</div>
</div>
<input class="btn btn-primary" type="submit" value="Log in">
</form>
From here on, this documentation uses the x-component syntax, which is the default way to build forms in Blade templates, and the BF facade whenever the context is PHP code.
The full correspondence table closes this page.
AI coding assistants
The repository ships a second documentation, written for an LLM rather than for you: dense, exhaustive, and pinned by the test suite instead of by prose.
Composer installs it along with the code, so it is already in your project, in the version you installed — nothing to download or paste.
Wire it in once. Add one line to the instructions file your tool reads at startup — CLAUDE.md, AGENTS.md, .cursor/rules/, .github/copilot-instructions.md, whichever yours is:
When building forms with `bgaze/bootstrap-form`, read
`vendor/bgaze/bootstrap-form/docs/llm/index.md` first, then load a spoke from its
on-demand index only if the task needs that area.
That is the whole setup, and it costs no network round trip since the guide is a local file.
What sits under vendor/bgaze/bootstrap-form/:
docs/llm/index.md— the hub. Read alone it covers the large majority of forms: the two mandatory detection steps, the field model, the three syntaxes, the full field catalog, the settings cascade.docs/llm/*.md— eight focused spokes (choice fields, layouts, input groups, model binding & validation, options & attributes, x-components, Bootstrap 4 ↔ 5, configuration), each loaded only when a task touches it.llms.txt— the index of the above, in the llms.txt convention.
The payoff is the two steps the hub opens on: read the resolved configuration — a published config/bootstrap_form.php wins over the packaged defaults — then detect the syntax already in use and match it.
That is what stops an assistant from dropping Bootstrap 4 markup into a Bootstrap 5 application, or from introducing a fourth way of declaring a form into a codebase that already has one.
A tool that browses rather than reads the disk takes the same files on GitHub: the guide hub and llms.txt.
Either way it is reference material, terse where this documentation explains: if you are reading rather than prompting, stay here.
The three syntaxes
The same API is exposed three times.
Whatever you pick, the rendered markup is identical: the x-components delegate to the facade, and each Blade directive is an alias of the matching facade method.
| Syntax | Where it fits | Status |
|---|---|---|
| x-components | Blade templates | the default since v4 |
BF facade |
PHP code — controllers, view composers, helpers | always available |
| Blade directives | Blade templates | supported, historical |
Mixing them is fine: a form opened with the facade can be closed by a directive.
The two Blade syntaxes can be turned off in the configuration (components, blade_directives) if an application only needs one of them.
x-components
Each field has a tag under the bf namespace, and <x-bf::form> wraps its fields as its default slot:
<x-bf::form url="/users" horizontal>
<x-bf::text name="name"/>
<x-bf::email name="email"/>
<x-bf::submit>Save</x-bf::submit>
</x-bf::form>
There is one tag per field, listed with its facade and directive counterparts in Available fields & syntaxes.
The constructor arguments of the matching facade method map to attributes, in the same order.
Use Blade's : binding to pass PHP values (:choices, :selected, :checked, :value, :label, :model):
<x-bf::form :model="$user" update="users.update">
<x-bf::text name="name"/>
<x-bf::select name="role" :choices="$roles" :selected="$user->role"/>
<x-bf::submit>Save</x-bf::submit>
</x-bf::form>
Two tags deviate slightly: <x-bf::link> takes href (plus title or its slot), and <x-bf::label> takes name — or the HTML idiom for — plus value or its slot.
Note also the kebab-case tag of the datetimeLocal method: <x-bf::datetime-local>.
Attribute projection
An x-component turns its attribute bag into the options array the facade expects, following these rules:
| Attribute | Becomes |
|---|---|
label:* |
an HTML attribute of the <label> element — label:class="fw-bold" |
group:* |
an HTML attribute of the form group — group:class="mb-4" |
group |
group="false" removes the wrapper entirely |
input:* |
a literal HTML attribute of the control, even when the name collides with a setting — input:size="10" |
option:* / optgroup:* |
attributes applied to every option / optgroup, on select, checkboxes and radios only |
| a setting name | normalized to its snake_case form — show-all-errors becomes show_all_errors |
a boolean setting written "true" / "false" |
the matching boolean, so escape="false" and :escape="false" agree |
| anything else | an HTML attribute of the control, verbatim — placeholder, required, data-*, aria-* |
Boolean attributes follow the Blade convention: a bare attribute passes true.
A boolean setting written as a string is normalized too, so escape="false" disables the setting instead of passing the string 'false', which is truthy — the same holds for switch, inline, custom, show-all-errors, show-valid-feedback and disable-errors.
Settings that legitimately accept a string are left alone: help="false" is the text false, not a boolean.
<x-bf::text name="email" required autofocus/>
<x-bf::checkbox name="accept" label="Accept" switch inline/>
Slots cover the cases where an attribute would be impractical:
| Slot | On | Effect |
|---|---|---|
| default slot | submit, reset, button, link, label |
the button, link or label text |
| default slot | form |
the fields of the form |
<x-slot:label> |
any field | the label, overriding the label attribute |
<x-slot:prepend> / <x-slot:append> |
text-like inputs and select |
input group addons |
<x-bf::submit>Log in</x-bf::submit>
<x-bf::text name="amount">
<x-slot:prepend>$</x-slot:prepend>
</x-bf::text>
The content of a slot is always raw markup. It reaches the facade as an HtmlString, so it is never escaped — including under a global escape, where the markup an author wrote in a template has to survive.
The facade equivalent of a slot is therefore new HtmlString('…') rather than a plain string.
The BF facade
The facade is the underlying API, and the natural choice outside Blade templates: controllers, view composers, helpers, or any code that builds markup as a string.
echo BF::open(['url' => '/users']);
echo BF::text('name', 'Full name', null, ['placeholder' => 'Jane Doe']);
echo BF::submit('Save');
echo BF::close();
Field methods return an object that renders itself when cast to a string, so they can be echoed, concatenated or returned to a view.
Blade directives
Every facade method has a directive of the same name, taking the same arguments:
@open(['url' => '/users'])
@text('name', 'Full name', null, ['placeholder' => 'Jane Doe'])
@submit('Save')
@close
They remain fully supported, but they are no longer the syntax to introduce into a new codebase — x-components read better in a template and get IDE support for free.
If you use PhpStorm, this gist configures syntax highlighting and live templates for the directives.
Bootstrap 4 & 5
Bootstrap 5 is the default. Bootstrap 4 is frozen but fully supported: no feature was dropped, and an application can stay on it indefinitely.
A version driver owns the whole class vocabulary, so switching version switches the markup — not just a few utility classes.
Selecting the version
Opt into a version at three levels, each one overriding the previous:
Application wide, in the published configuration file:
// config/bootstrap_form.php
'bootstrap_version' => 4,
Per form — every field of the form inherits it:
<x-bf::form url="/users" bootstrap-version="4"> … </x-bf::form>
echo BF::open(['url' => '/users', 'bootstrap_version' => 4]);
Per field:
<x-bf::text name="login" bootstrap-version="4"/>
A per-field override switches the driver — the component classes — but not the layout settings, which stay inherited from the form.
So a Bootstrap 4 field inside a Bootstrap 5 form keeps the form group class of that form:
<div id="login-group" class="mb-3">
<label for="login">Login</label>
<div>
<input id="login" class="form-control" name="login" type="text">
</div>
</div>
Style the group, or pin the version at form level, when you need the whole markup of the other version.
What changes between versions
| Area | Bootstrap 5 | Bootstrap 4 |
|---|---|---|
| Form group | mb-3 — the group_class setting, not driver code |
form-group |
| Label | form-label |
no class |
| Select | form-select |
form-control, or custom-select |
| Range | form-range |
form-control-range, or custom-range |
| Checkbox & radio | form-check |
form-check, or custom-control custom-checkbox / custom-radio |
| Switch | form-check form-switch plus role="switch" |
custom-control custom-switch |
| File | form-control |
native, or custom-file markup |
| Input group addons | addons are direct children of the input group | each one nested in an input-group-prepend / input-group-append div |
custom option |
no-op — Bootstrap 5 unified both | native or custom controls |
| Floating labels | supported | not supported, degrades to vertical |
| Horizontal layout | no class, the grid carries it | form-horizontal on the form tag |
| Inline layout | best effort, Bootstrap 5 reworked inline forms | form-inline |
| Inline spacing | me-* / ms-* |
mr-* / ml-* |
Three representative fields, in both versions.
A text input — the label carries a class in Bootstrap 5, and the form group is a spacing utility rather than a component:
Bootstrap 5
<div id="login-group" class="mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control" name="login" type="text">
</div>
</div>
Bootstrap 4
<div id="login-group" class="form-group">
<label for="login">Login</label>
<div>
<input id="login" class="form-control" name="login" type="text">
</div>
</div>
A switch — the checkable markup is where the two versions differ the most:
Bootstrap 5
<div id="accept-group" class="mb-3">
<div>
<div class="form-check form-switch">
<input id="accept" class="form-check-input" role="switch" name="accept" type="checkbox" value="1">
<label for="accept" class="form-check-label">Accept</label>
</div>
</div>
</div>
Bootstrap 4
<div id="accept-group" class="form-group">
<div>
<div class="custom-control custom-switch">
<input id="accept" class="custom-control-input" name="accept" type="checkbox" value="1">
<label for="accept" class="custom-control-label">Accept</label>
</div>
</div>
</div>
An input group — Bootstrap 4 wraps each addon in its own div:
Bootstrap 5
<div id="amount-group" class="mb-3">
<label for="amount" class="form-label">Amount</label>
<div>
<div class="input-group">
<span class="input-group-text">$</span>
<input id="amount" class="form-control" name="amount" type="text">
<span class="input-group-text">.00</span>
</div>
</div>
</div>
Bootstrap 4
<div id="amount-group" class="form-group">
<label for="amount">Amount</label>
<div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">$</span>
</div>
<input id="amount" class="form-control" name="amount" type="text">
<div class="input-group-append">
<span class="input-group-text">.00</span>
</div>
</div>
</div>
</div>
Only the layout-level options of the bootstrap4 / bootstrap5 configuration sections are tunable; component classes are driver code.
Forms
Creating forms
A form is opened with <x-bf::form> (or BF::open() / @open), and its destination is set with one of url, route or action:
<x-bf::form url="/users"> … </x-bf::form>
<x-bf::form route="users.store"> … </x-bf::form>
<x-bf::form :route="['users.update', $user]"> … </x-bf::form>
<form method="POST" action="https://example.com/users" accept-charset="UTF-8" role="form">
<input name="_token" type="hidden">
</form>
- The method defaults to POST;
method="get"renders a GET form, andPUT/PATCH/DELETEare spoofed through a hidden_methodfield. - A CSRF token field is appended automatically to every non-GET form, its value filled in by Laravel.
filesaddsenctype="multipart/form-data".
An edit form uploading a file uses both:
<x-bf::form url="/users/1" method="put" files> … </x-bf::form>
<form method="POST" action="https://example.com/users/1" accept-charset="UTF-8" role="form" enctype="multipart/form-data">
<input name="_method" type="hidden" value="PUT">
<input name="_token" type="hidden">
</form>
Anything else passed to the form is either one of the settings below — inherited by its fields — or an HTML attribute of the <form> element (id, class, novalidate, data-*).
Form options
These settings are read on the form and inherited by every field it contains; a field may override any of them.
| Option | Default value | Accepted values | Description |
|---|---|---|---|
| layout | vertical |
vertical / horizontal / inline / floating | The Bootstrap layout of the form |
| bootstrap_version | 5 |
4 / 5 | The markup version to render |
| group | [] |
false / array | HTML attributes of every form group, or false to drop the wrappers |
| show_all_errors | false |
bool | Show all the errors of a field instead of only the first |
| show_valid_feedback | false |
bool | Mark error-free fields as valid after a failed submit |
| required_mark | ' *' |
false / string | Mark appended to the label of a required field, HTML accepted |
| escape | false |
bool | Escape the content sinks of every field, see Content escaping |
| error_bag | default |
string | The name of the error bag the fields read |
| custom 1 | false |
bool | Use the Bootstrap 4 custom-styled controls |
| left_class 2 | col-lg-2 col-xl-3 |
string | Width of the label column |
| right_class 2 | col-lg-10 col-xl-9 |
string | Width of the control column |
| pull_right 2 | d-none d-lg-block col-lg-2 col-xl-3 |
false / string | Empty left column keeping label-less fields aligned |
| lspace 3 | me-2 |
false / string | Space between a label and its control |
| hspace 3 | me-3 |
false / string | Horizontal space between groups |
| vspace 3 | my-1 |
false / string | Vertical space between groups |
1: Bootstrap 4 only — a recognized no-op in Bootstrap 5, where custom controls were merged into the defaults.
2: Horizontal layout only.
3: Inline layout only.
The defaults of the last six come from the version section of the configuration file, so they differ between Bootstrap 4 and 5.
Six options are reserved to the form and never inherited by its fields: model, url, route, action, store and update.
Form layouts
The layout is a form-level setting. Set it with the layout option, or with the boolean shortcut of the same name:
<x-bf::form url="/x" horizontal> … </x-bf::form>
<x-bf::form url="/x" layout="horizontal"> … </x-bf::form>
// same as BF::open(['url' => '/x', 'layout' => 'horizontal'])
echo BF::horizontal(['url' => '/x']);
The previews below show the fields of each form; the surrounding <form> element is omitted.
Vertical form
The default: label above the control, each field wrapped in a form group.
<x-bf::form url="/login" vertical>
<x-bf::text name="login" label="Login"/>
<x-bf::password name="password" label="Password"/>
<x-bf::checkbox name="remember" label="Remember me" switch/>
<x-bf::submit>Log in</x-bf::submit>
</x-bf::form>
Horizontal form
Label and control side by side on a grid row.
left_class and right_class set the column widths; pull_right renders an empty left column for label-less fields, so they stay aligned — that is what keeps the checkbox below in line with the inputs.
<x-bf::form url="/signup" horizontal>
<x-bf::email name="mail" label="Email"/>
<x-bf::tel name="phone" label="Phone" help="Mobile number preferred"/>
<x-bf::checkbox name="accept" label="Accept the terms"/>
<x-bf::submit>Sign up</x-bf::submit>
</x-bf::form>
Inline form
Bootstrap 5 removed the form-inline class: lining a form up is now the job of the flex utilities, applied where you want them.
The inline layout therefore does not lay anything out on its own — what it brings is the spacing, through lspace between a label and its control, hspace between groups and vspace between wrapped rows.
Two classes turn that into a form on one line:
- on the form,
d-flex flex-wrap align-items-centerputs the groups in a row; - on every group,
d-flex align-items-centerputs each label next to its own control.
Since supplying a group class takes over the spacing, restate hspace and vspace there — and give lspace a mb-0, which cancels the bottom margin a label carries by default.
Both settings are inherited, so they are declared once on the form:
<x-bf::form url="/search" inline class="d-flex flex-wrap align-items-center"
group:class="d-flex align-items-center me-3 my-1" lspace="me-2 mb-0">
<x-bf::text name="keywords" label="Keywords"/>
<x-bf::select name="city" label="City" :choices="['pa' => 'Paris', 'ly' => 'Lyon']"/>
<x-bf::submit>Search</x-bf::submit>
</x-bf::form>
On Bootstrap 4 the same form needs none of this: form-inline did the work, and the layout is enough on its own.
Floating form
Bootstrap 5 only. The label floats over the control, which is rendered first and wrapped in a form-floating block.
Text-like inputs, textarea and select float; the other field types render normally inside a floating form.
On Bootstrap 4 the layout degrades to vertical.
<x-bf::form url="/contact" floating>
<x-bf::email name="contact" label="Email address"/>
<x-bf::select name="country" label="Country" :choices="['fr' => 'France', 'be' => 'Belgium']"/>
<x-bf::textarea name="message" label="Your message"/>
</x-bf::form>
Text-like controls get an injected placeholder=" ", which the Bootstrap CSS requires; select does not:
<div id="email-group" class="mb-3">
<div>
<div class="form-floating">
<input id="email" class="form-control" placeholder=" " name="email" type="text">
<label for="email">Email address</label>
</div>
</div>
</div>
Model binding
Bind an Eloquent model with model, and pick the endpoint with store or update:
<x-bf::form :model="$user" store="users.store"> … </x-bf::form>
<x-bf::form :model="$user" update="users.update"> … </x-bf::form>
- If the model exists and
updateis set, the method becomesPUTand the route key of the model is appended to the route automatically. - If the model does not exist and
storeis set, the method staysPOST. storeandupdateaccept the same forms asrouteandaction: a name, aController@methodstring, or an array of a name and its parameters.
Fields then read their value from the model, without being given one:
<div id="login-group" class="mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control" name="login" type="text" value="jdoe">
</div>
</div>
A field value is resolved in this order: old input, then the explicit value argument, then the bound model attribute, then null.
Field names are transformed for the lookup, so user[email] reads user.email. A model may also define a getFormValue($key) method to take over the resolution.
After a failed validation redirect, fields repopulate from the flashed old input, including the selection of a select and the checked state of checkboxes and radios:
<div id="login-group" class="mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control" name="login" type="text" value="old value">
</div>
</div>
One caveat worth knowing: with the ConvertEmptyStringsToNull middleware enabled — the Laravel default — a field the user cleared before a failed submit comes back empty rather than repopulated from the model, which is what one expects.
Form inputs
Creating inputs
Almost every field shares the same four arguments — name, label, value, options — which the x-components expose as attributes of the same name:
- name — required. Unless you provide them, it also derives the
idof the control and the id of its form group. Array names such asuser[email]are supported: the id flattens them (user-email) while the error lookup uses the dotted form (user.email). - label —
nullor omitted generates it from the name, in title case.falserenders no label. A string sets the text, and an array sets the HTML attributes of the<label>element. - value — the value of the field. It is overridden at render time by old input and by model binding, so most of the time there is nothing to pass.
- options — an associative array. Any key the field does not recognize as a setting is rendered as an HTML attribute of the control.
A name is all a field needs — the label, the id and the group are derived from it:
<x-bf::text name="simple"/>
<div id="simple-group" class="mb-3">
<label for="simple" class="form-label">Simple</label>
<div>
<input id="simple" class="form-control" name="simple" type="text">
</div>
</div>
Any key the field does not recognize lands on the control as an HTML attribute, here a value and a bare disabled:
<x-bf::text name="disabled" value="custom value" disabled/>
<div id="disabled-group" class="mb-3">
<label for="disabled" class="form-label">Disabled</label>
<div>
<input disabled id="disabled" class="form-control" name="disabled" type="text" value="custom value">
</div>
</div>
The generated label and id are overridden the same way:
<x-bf::text name="custom" label="Custom" id="custom-id" placeholder="Enter a value"/>
<div id="custom-group" class="mb-3">
<label for="custom-id" class="form-label">Custom</label>
<div>
<input id="custom-id" placeholder="Enter a value" class="form-control" name="custom" type="text">
</div>
</div>
help is a setting, not an attribute: it renders a hint under the control, wired to it for screen readers:
<x-bf::text name="help_text" label="Field with help text" help="This is a help text"/>
<div id="help_text-group" class="mb-3">
<label for="help_text" class="form-label">Field with help text</label>
<div>
<input id="help_text" class="form-control" aria-describedby="help_text-help" name="help_text" type="text">
<small id="help_text-help" class="form-text">This is a help text</small>
</div>
</div>
The form group takes attributes of its own through the group: prefix:
<x-bf::text name="custom_group" label="Custom form group" group:class="p-2 border"/>
<div class="p-2 border" id="custom_group-group">
<label for="custom_group" class="form-label">Custom form group</label>
<div>
<input id="custom_group" class="form-control" name="custom_group" type="text">
</div>
</div>
And it can be dropped entirely, leaving the bare control:
<x-bf::text name="no_form_group" label="No form group" group="false"/>
<input id="no_form_group" class="form-control" name="no_form_group" type="text">
Common options
In addition to their own, every field accepts these options.
Inherited means the value comes from the current form, or from the package configuration when no form is open.
| Option | Default value | Accepted values | Description |
|---|---|---|---|
| label | null |
null / false / string / array | The label of the field, see above |
| help | false |
false / string | A help text rendered under the control |
| success | false |
false / string | The valid feedback message of the field |
| group | Inherited | false / array | HTML attributes of the form group, or false to render the control alone |
| layout | Inherited | vertical / horizontal / inline / floating | Overrides the layout of the form for this field |
| bootstrap_version | Inherited | 4 / 5 | Overrides the markup version for this field |
| show_all_errors | Inherited | bool | Show all the errors of the field instead of only the first |
| show_valid_feedback | Inherited | bool | Mark the field valid when it carries no error |
| required_mark | Inherited | false / string | The mark appended to the label of a required field |
| escape | Inherited | bool | Escape the content sinks of the field instead of emitting raw HTML, see Content escaping |
| error_bag | Inherited | string | The error bag the field reads |
| custom | Inherited | bool | Bootstrap 4 custom-styled control |
| size 1 | false |
false / sm / lg | The Bootstrap size of the control |
| prepend 1 | false |
false / string / array | Input group addon before the control |
| append 1 | false |
false / string / array | Input group addon after the control |
1: Text-like inputs and select only — see Input groups and Help text & sizing.
Options & HTML attributes
The options array is split into two disjoint sets: keys the field knows as settings, consumed as configuration and never rendered, and everything else, rendered on the control as HTML attributes.
echo BF::text('login', 'Login', null, [
'help' => 'Your username', // setting → a help text
'placeholder' => 'jdoe', // attribute → placeholder="jdoe"
'required' => true, // attribute → a valueless `required`
]);
<div id="login-group" class="mb-3">
<label for="login" class="form-label">Login *</label>
<div>
<input placeholder="jdoe" required id="login" class="form-control" aria-describedby="login-help" name="login" type="text">
<small id="login-help" class="form-text">Your username</small>
</div>
</div>
An attribute set to true renders valueless, one set to false or null is omitted, and class is merged rather than replaced.
To force a key onto the element although its name collides with a setting, prefix it with ~ — or use the input: prefix in an x-component.
Without it, size would be read as the Bootstrap control size:
<x-bf::text name="code" label="Code" input:size="10"/>
echo BF::text('code', 'Code', null, ['~size' => '10']);
<div id="code-group" class="mb-3">
<label for="code" class="form-label">Code</label>
<div>
<input size="10" id="code" class="form-control" name="code" type="text">
</div>
</div>
Ids are generated from the name unless you pass one, and id="false" removes the attribute entirely — the label then renders without its for.
The derived ids follow from it: {id}-group for the wrapper, {id}-help, {id}-error and {id}-valid for the messages.
<x-bf::text name="login" :id="false"/>
<div id="login-group" class="mb-3">
<label class="form-label">Login</label>
<div>
<input class="form-control" name="login" type="text">
</div>
</div>
Supply a class for an element and you own its styling. The package then adds only the classes its version driver requires, and skips the ones coming from the configuration.
There is exactly one channel per element, and it always replaces:
| Element | Your attribute | Always added | Skipped |
|---|---|---|---|
| Form group | group:class |
row in horizontal layout, is-invalid / is-valid |
group_class, hspace, vspace |
| Label | label:class |
form-label, col-form-label, form-check-label, pt-0 |
left_class, lspace |
| Control | class |
form-control, form-select, form-check-input, size and state classes |
none |
The same field, left alone, then styled, then unstyled:
<x-bf::text name="q" label="Search"/>
<x-bf::text name="q" label="Search" group:class="mb-4"/>
<x-bf::text name="q" label="Search" :group:class="false"/>
<div id="q-group" class="mb-3">
<label for="q" class="form-label">Search</label>
<div>
<input id="q" class="form-control" name="q" type="text">
</div>
</div>
<div class="mb-4" id="q-group">
<label for="q" class="form-label">Search</label>
<div>
<input id="q" class="form-control" name="q" type="text">
</div>
</div>
<div id="q-group">
<label for="q" class="form-label">Search</label>
<div>
<input id="q" class="form-control" name="q" type="text">
</div>
</div>
The driver classes always survive, so a styled element never stops being the right Bootstrap component.
Everything else is yours — including the column width of a horizontal layout, which you must restate when you style a label:
<x-bf::text name="q" label="Search" label:class="fw-bold col-lg-2 col-xl-3"/>
Set the application-wide defaults in the configuration rather than at call sites.
Content escaping
Five settings are content sinks: they emit their value as HTML.
The label of a field, its help and success messages, the prepend / append addons, and the text browse label of a Bootstrap 4 custom file input.
Raw is the default on purpose — putting markup in a label or an addon is a real need.
escape flips that regime, and it is inherited like the other settings: configuration, then form, then field.
It defaults to false, so an existing application renders exactly as before. Added in v4.2.
echo BF::text('q', '<b>Bold</b> & co');
echo BF::text('q', '<b>Bold</b> & co', null, ['escape' => true]);
<div id="q-group" class="mb-3">
<label for="q" class="form-label"><b>Bold</b> & co</label>
<div>
<input id="q" class="form-control" name="q" type="text">
</div>
</div>
<div id="q-group" class="mb-3">
<label for="q" class="form-label"><b>Bold</b> & co</label>
<div>
<input id="q" class="form-control" name="q" type="text">
</div>
</div>
Addons are where the setting earns its keep. By default the escaping decision is taken by the value rather than by the call site: a value carrying a tag passes through verbatim, a value without one is escaped and wrapped in an input-group-text span — the rule described in Input groups.
That is the part which surprises: the same call site is escaped for °C, and raw the day its value happens to contain a tag.
With escape the heuristic retires — an addon value is text, always — and the decision belongs to the call site.
The three sinks of a single field, all escaped:
echo BF::text('q', '<b>Q</b> & co', null, [
'help' => '<i>Help</i> & co',
'prepend' => '<b>$</b>',
'escape' => true,
]);
<div id="q-group" class="mb-3">
<label for="q" class="form-label"><b>Q</b> & co</label>
<div>
<div class="input-group">
<span class="input-group-text"><b>$</b></span>
<input id="q" class="form-control" aria-describedby="q-help" name="q" type="text">
</div>
<small id="q-help" class="form-text"><i>Help</i> & co</small>
</div>
</div>
A value that is already markup is never escaped. Anything implementing Htmlable — an HtmlString, the slot of an x-component — is markup by construction, and the setting does not reach it.
That is the per-value escape hatch, and it is what keeps slots working under a global escape: the content of a slot is markup its author wrote in a template, so it is handed to the facade as an HtmlString.
<x-bf::text name="amount" label="Amount" escape>
<x-slot:append><button type="button" class="btn btn-outline-secondary">Go</button></x-slot:append>
</x-bf::text>
echo BF::text('amount', 'Amount', null, [
'escape' => true,
'append' => new HtmlString('<button type="button" class="btn btn-outline-secondary">Go</button>'),
]);
<div id="amount-group" class="mb-3">
<label for="amount" class="form-label">Amount</label>
<div>
<div class="input-group">
<input id="amount" class="form-control" name="amount" type="text">
<button type="button" class="btn btn-outline-secondary">Go</button>
</div>
</div>
</div>
The bypass skips the escaping decision, not the wrapping one.
A tag-free Htmlable is still wrapped as a text addon, which is precisely why <x-slot:prepend>$</x-slot:prepend> keeps its input-group-text span while the button above stays bare.
Three things stay outside the setting:
| Never escaped | Why |
|---|---|
required_mark |
It is configuration, authored by a developer, and its HTML is a documented feature — so it is concatenated after the label has been escaped. See Required mark. |
| Error messages | Validation feedback keeps a path of its own; of the field messages, only success is a sink. |
BF::label() |
The standalone label carries its own per-call switch, a fourth argument, and ignores the setting. See Label. |
Escaping goes through htmlspecialchars with ENT_QUOTES and no double encoding — the same policy as HTML attributes, textarea content and select options — so a value already carrying an encoded entity survives a round trip instead of turning into &amp;.
It is not a substitute for escaping at the boundary of the application.
A value coming from user input, from the database or from a language file should be escaped before it reaches a sink, whether the setting is on or not.
Text inputs
The text-like inputs — every one of them listed in Available fields & syntaxes — share the four arguments and the addon, size and help options.
Two deviate: password takes no value argument, so its signature is (name, label, options), and the name argument of email defaults to email.
<x-bf::number name="qty" label="Quantity" min="1" max="99" step="1"/>
<div id="qty-group" class="mb-3">
<label for="qty" class="form-label">Quantity</label>
<div>
<input min="1" max="99" step="1" id="qty" class="form-control" name="qty" type="number">
</div>
</div>
textarea renders the matching element, and reads size as a cols x rows pair (size="30x5") rather than as a Bootstrap size:
<div id="bio-group" class="mb-3">
<label for="bio" class="form-label">Bio</label>
<div>
<textarea id="bio" class="form-control" name="bio" cols="50" rows="10"></textarea>
</div>
</div>
Select input
select takes the choices as its third argument and the selection as its fourth:
<x-bf::select name="role" :choices="['a' => 'A', 'b' => 'B', 'c' => 'C']" selected="b"/>
echo BF::select('role', null, ['a' => 'A', 'b' => 'B', 'c' => 'C'], 'b');
<div id="role-group" class="mb-3">
<label for="role" class="form-label">Role</label>
<div>
<select id="role" class="form-select" name="role">
<option value="a">A</option>
<option value="b" selected="selected">B</option>
<option value="c">C</option>
</select>
</div>
</div>
Beyond the common options, select recognizes choices, selected, placeholder, custom, option_attributes and optgroup_attributes.
Anything else — multiple, required, data-* — is an HTML attribute of the element.
- selected accepts a scalar or an array; every matching option is marked selected. For a multiple selection, add the
multipleattribute and pass an array. - placeholder is not an HTML attribute here: it renders a leading, pre-selected blank option.
<div id="role-group" class="mb-3">
<label for="role" class="form-label">Role</label>
<div>
<select id="role" class="form-select" name="role">
<option selected="selected" value="">Pick one</option>
<option value="a">A</option>
</select>
</div>
</div>
The choices grammar
choices accepts an array, a Collection or any other iterable, so a Model::pluck('name', 'id') can be passed straight through.
Four entry forms can be freely mixed:
| Form | Syntax | Renders |
|---|---|---|
| Simple option | 'a' => 'A' |
<option value="a">A</option> |
| Simple optgroup | 'G1' => ['a' => 'A', 'b' => 'B'] |
an <optgroup> holding both options |
| Advanced option | ['value' => 'b', 'label' => 'B', 'disabled' => true] |
an <option> carrying the extra keys as attributes |
| Advanced optgroup | ['label' => 'Group', 'options' => [...], 'class' => 'grp'] |
an <optgroup> carrying the extra keys as attributes |
Parsing is strict, and an incomplete descriptor throws an InvalidArgumentException: an advanced option must define both value and label, an advanced optgroup must define label and an iterable options, and optgroups cannot be nested.
The four forms in one list — a plain option, a group, an option carrying its own attributes, then a group carrying its own:
<x-bf::select name="role" :choices="[
'admin' => 'Admin',
'Editors' => ['chief' => 'Chief editor', 'editor' => 'Editor'],
['value' => 'guest', 'label' => 'Guest', 'disabled' => true],
['label' => 'Archived', 'class' => 'text-muted',
'options' => ['legacy' => 'Legacy role']],
]"/>
<div id="role-group" class="mb-3">
<label for="role" class="form-label">Role</label>
<div>
<select id="role" class="form-select" name="role">
<option value="admin">Admin</option>
<optgroup label="Editors">
<option value="chief">Chief editor</option>
<option value="editor">Editor</option>
</optgroup>
<option value="guest" disabled>Guest</option>
<optgroup label="Archived" class="text-muted">
<option value="legacy">Legacy role</option>
</optgroup>
</select>
</div>
</div>
option_attributes and optgroup_attributes apply to every option or optgroup, the attributes of an advanced entry winning over them.
In an x-component, use the option: and optgroup: prefixes:
<x-bf::select name="role" :choices="['a' => 'A', 'b' => 'B']" option:class="opt"/>
Checkbox & radio
A single checkbox or radio takes its submitted value as third argument and its checked state as fourth.
A checkbox defaults to the value 1, a radio to null.
<x-bf::checkbox name="accept" label="Accept"/>
<div id="accept-group" class="mb-3">
<div>
<div class="form-check">
<input id="accept" class="form-check-input" name="accept" type="checkbox" value="1">
<label for="accept" class="form-check-label">Accept</label>
</div>
</div>
</div>
Three settings are specific to them: switch renders a checkbox as a switch, inline puts it on the flow line, and custom opts into the Bootstrap 4 custom controls.
<x-bf::checkbox name="notify" label="Email me about updates" checked switch/>
<x-bf::radios name="theme" label="Theme"
:choices="['light' => 'Light', 'dark' => 'Dark']" checked="light" inline/>
<x-bf::checkboxes name="perms" label="Permissions"
:choices="['read' => 'Read', 'write' => 'Write']"
:checked="['read']" help="Write implies read."/>
The help text and the validation feedback of a checkable are placed differently from the other fields: the feedback goes inside the form-check wrapper, where Bootstrap expects it as a sibling of the input, while the help text goes after the wrapper, full width.
<div id="accept-group" class="mb-3">
<div>
<div class="form-check">
<input id="accept" class="form-check-input" aria-describedby="accept-help" name="accept" type="checkbox" value="1">
<label for="accept" class="form-check-label">Accept</label>
</div>
<small id="accept-help" class="form-text">Some help</small>
</div>
</div>
Checkboxes & radios
checkboxes and radios render a whole group from a choice list, under a single global label:
<x-bf::radios name="gender" label="Gender" :choices="['m' => 'Male', 'f' => 'Female']" checked="f"/>
<div id="gender-group" class="mb-3">
<label for="gender" class="form-label">Gender</label>
<div>
<div class="form-check">
<input id="gender-m" class="form-check-input" name="gender" type="radio" value="m">
<label for="gender-m" class="form-check-label">Male</label>
</div>
<div class="form-check">
<input id="gender-f" class="form-check-input" checked="checked" name="gender" type="radio" value="f">
<label for="gender-f" class="form-check-label">Female</label>
</div>
</div>
</div>
- choices follows the same grammar as
select, minus optgroups: anoptionskey or a nested group throws. - checked accepts a scalar or an array of checked values, so a multi-valued field binds naturally.
- The id of each child is
{name}-{value}, unless an advanced entry sets its own. - inline propagates to every child, and
option_attributes— or theoption:prefix — applies to all of them. - escape propagates too: the individual choice labels are escaped along with the global one, nothing to declare per child.
<div id="roles-group" class="mb-3">
<label for="roles" class="form-label">Roles</label>
<div>
<div class="form-check">
<input id="roles-admin" class="form-check-input" checked="checked" name="roles" type="checkbox" value="admin">
<label for="roles-admin" class="form-check-label">Admin</label>
</div>
<div class="form-check">
<input id="roles-editor" class="form-check-input" name="roles" type="checkbox" value="editor">
<label for="roles-editor" class="form-check-label">Editor</label>
</div>
<div class="form-check">
<input id="roles-viewer" class="form-check-input" checked="checked" name="roles" type="checkbox" value="viewer">
<label for="roles-viewer" class="form-check-label">Viewer</label>
</div>
</div>
</div>
A collection has no single control, so two behaviours differ from a standalone checkable: the validation feedback is rendered once, at the collection level, and the required mark is appended to the global label only, never to the individual choice labels.
<div id="roles-group" class="mb-3">
<label for="roles" class="form-label">Roles *</label>
<div>
<div class="form-check">
<input required id="roles-admin" class="form-check-input" name="roles" type="checkbox" value="admin">
<label for="roles-admin" class="form-check-label">Admin</label>
</div>
<div class="form-check">
<input required id="roles-editor" class="form-check-input" name="roles" type="checkbox" value="editor">
<label for="roles-editor" class="form-check-label">Editor</label>
</div>
</div>
</div>
File input
file takes no value argument — a file input cannot be prefilled — so its signature is (name, label, options).
Remember to open the form with files so it carries the right encoding type.
<x-bf::form url="/import" files>
<x-bf::file name="docs" label="Documents" multiple/>
</x-bf::form>
<div id="docs-group" class="mb-3">
<label for="docs" class="form-label">Documents</label>
<div>
<input multiple id="docs" class="form-control" name="docs" type="file">
</div>
</div>
Bootstrap 4 only — the custom file control. Under custom, Bootstrap 4 renders its styled custom-file widget, which carries two settings of its own: text is the label shown inside the control, 'Choose file' by default, and button sets the label of the browse button through the data-browse attribute.
Both are swallowed anywhere else — the field must be custom and the version must be 4, since Bootstrap 5 unified the two presentations and turned custom into a no-op.
echo BF::file('avatar', null, [
'custom' => true,
'text' => 'Pick a file',
'button' => 'Browse',
]);
<div id="avatar-group" class="form-group">
<label for="avatar">Avatar</label>
<div>
<div class="custom-file">
<input id="avatar" class="custom-file-input" name="avatar" type="file">
<label for="avatar" class="custom-file-label" data-browse="Browse">Pick a file</label>
</div>
</div>
</div>
text is a content sink — raw HTML by default, escaped under escape.
button lands in an attribute, so it is always escaped:
echo BF::file('avatar', null, [
'custom' => true,
'text' => '<b>Pick</b> a file',
'escape' => true,
]);
<div id="avatar-group" class="form-group">
<label for="avatar">Avatar</label>
<div>
<div class="custom-file">
<input id="avatar" class="custom-file-input" name="avatar" type="file">
<label for="avatar" class="custom-file-label"><b>Pick</b> a file</label>
</div>
</div>
</div>
Range input
range takes the four common arguments, and the min, max and step HTML attributes drive its steps:
<x-bf::range name="vol" label="Volume" value="50" min="0" max="100" step="5"/>
<div id="vol-group" class="mb-3">
<label for="vol" class="form-label">Volume</label>
<div>
<input min="0" max="100" step="5" id="vol" class="form-range" name="vol" type="range" value="50">
</div>
</div>
Validation & feedback
Nothing has to be wired: a field reads the session error bag by itself, marks the control and renders the message.
<x-bf::form url="/profile" show-valid-feedback>
<x-bf::text name="nickname" label="Nickname"/>
<x-bf::text name="handle" label="Handle" success="Looks good!"/>
<x-bf::checkbox name="conditions" label="I accept the conditions" help="Required to continue."/>
</x-bf::form>
Rendered after a failed submit where only nickname was rejected: the values come back from the old input, and the two error-free fields are marked valid.
Error display
A field carrying an error gets is-invalid on its control and on its form group, an aria-invalid="true", and an invalid-feedback message wired to the control through aria-describedby:
<div id="login-group" class="is-invalid mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control is-invalid" aria-describedby="login-error" aria-invalid="true" name="login" type="text">
<div class="invalid-feedback" id="login-error">
The login field is required.
</div>
</div>
</div>
- show_all_errors renders every message of the field instead of only the first.
- error_bag targets a named bag, which is what several forms on the same page need.
- When a field has both an error and a help text, both ids are referenced by
aria-describedby.
<div id="login-group" class="is-invalid mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control is-invalid" aria-describedby="login-error" aria-invalid="true" name="login" type="text">
<div class="invalid-feedback" id="login-error">
The login field is required.
</div>
<div class="invalid-feedback" id="login-error">
The login must be at least 3 characters.
</div>
</div>
</div>
Two placements deviate, for markup reasons.
On a standalone checkbox or radio, the message sits inside the form-check wrapper, right after the label: Bootstrap only displays it as a sibling of the input, so no d-block is needed there.
<div id="accept-group" class="is-invalid mb-3">
<div>
<div class="form-check">
<input id="accept" class="form-check-input is-invalid" aria-describedby="accept-error accept-help" aria-invalid="true" name="accept" type="checkbox" value="1">
<label for="accept" class="form-check-label">Accept</label>
<div class="invalid-feedback" id="accept-error">
You must accept.
</div>
</div>
<small id="accept-help" class="form-text">Some help</small>
</div>
</div>
On a collection — checkboxes, radios — the message is rendered once, after the choices, and always as a block, since the collection has no single control to attach it to.
Valid feedback
Marking valid fields is opt-in, because it only makes sense after a submit that produced errors.
Turn show_valid_feedback on, globally or per form, and every field that carries no error of its own is marked is-valid with aria-invalid="false".
A success message then renders a valid-feedback:
<div id="login-group" class="is-valid mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control is-valid" aria-describedby="login-valid" aria-invalid="false" name="login" type="text">
<div class="valid-feedback" id="login-valid">
Looks good!
</div>
</div>
</div>
Valid and invalid states are mutually exclusive: a field is only marked valid when it has no error.
On a checkable, the valid feedback sits inside the form-check wrapper, like the error message.
Required mark
Setting the HTML required attribute on a field also appends the required mark to its label, ' *' by default:
<x-bf::text name="email" label="Email" required/>
<div id="email-group" class="mb-3">
<label for="email" class="form-label">Email *</label>
<div>
<input required id="email" class="form-control" name="email" type="text">
</div>
</div>
The mark is a setting, never an HTML attribute, and it accepts HTML verbatim — spacing included, which is why the default value starts with a space:
echo BF::text('email', 'Email', null, [
'required' => true,
'required_mark' => ' <span class="text-danger">*</span>',
]);
<div id="email-group" class="mb-3">
<label for="email" class="form-label">Email <span class="text-danger">*</span></label>
<div>
<input required id="email" class="form-control" name="email" type="text">
</div>
</div>
Set it to false to disable it, globally, per form or per field.
On a collection it marks the global label only, never the individual choice labels.
Because its HTML is a documented feature, the mark is never escaped: it is concatenated after the label, so it survives escape untouched.
Misc
Hidden input
hidden takes a name and a value, and renders neither label nor form group:
<x-bf::hidden name="token" value="abc123"/>
<input id="token" name="token" type="hidden" value="abc123">
Input groups
prepend and append wrap the control in a Bootstrap input group.
They are available on the text-like inputs and on select, and each accepts a string or an array of items.
Text and HTML are told apart automatically: a value with no HTML tag is escaped and wrapped in an input-group-text span for you, while a value containing a tag is emitted verbatim, so you own the markup of a button, a dropdown or an icon.
<x-bf::text name="budget" label="Budget" prepend="$" append=".00"/>
<x-bf::text name="site" label="Website" prepend="https://"/>
<x-bf::text name="query" label="Search"
append='<button type="button" class="btn btn-outline-secondary">Go</button>'/>
The detection keys on a tag opening, so bare content such as °C, $ or R&D stays text.
Addons can also be passed as slots, resolved the same way — the two writings below render the same markup:
<x-bf::text name="amount" label="Amount">
<x-slot:prepend>$</x-slot:prepend>
<x-slot:append>.00</x-slot:append>
</x-bf::text>
echo BF::text('amount', 'Amount', null, ['prepend' => '$', 'append' => '.00']);
<div id="amount-group" class="mb-3">
<label for="amount" class="form-label">Amount</label>
<div>
<div class="input-group">
<span class="input-group-text">$</span>
<input id="amount" class="form-control" name="amount" type="text">
<span class="input-group-text">.00</span>
</div>
</div>
</div>
An array renders one addon per item, mixing both forms freely:
echo BF::text('price', 'Price', null, [
'prepend' => ['$', '<span class="input-group-text">USD</span>'],
]);
The escaping decision is taken by the value, not by the call site — which is exactly what the escape setting changes.
With escape on, an addon value is text and nothing else; the heuristic retires.
A value implementing Htmlable — an HtmlString, or a slot — is markup by construction and stays raw either way:
| Value | escape => false (default) |
escape => true |
|---|---|---|
| String, no tag | escaped, wrapped in a span | escaped, wrapped in a span |
| String carrying a tag | raw, unwrapped | escaped, wrapped in a span |
Htmlable, no tag |
escaped, wrapped in a span | escaped, wrapped in a span |
Htmlable carrying a tag |
raw, unwrapped | raw, unwrapped |
Only the second row moves, and it is the one that matters: an addon fed from user input, from a database column or from a language file is an injection sink until either it is escaped upstream or the setting is on.
Inside an input group, the validation feedback is forced to display as a block.
Help text & sizing
help renders a <small class="form-text"> after the control, wired to it for screen readers through aria-describedby:
<div id="login-group" class="mb-3">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control" aria-describedby="login-help" name="login" type="text">
<small id="login-help" class="form-text">Some help</small>
</div>
</div>
size sizes the control, and the input group around it, with sm or lg:
<x-bf::text name="code" label="Code" size="sm"/>
<div id="code-group" class="mb-3">
<label for="code" class="form-label">Code</label>
<div>
<input id="code" class="form-control form-control-sm" name="code" type="text">
</div>
</div>
On a textarea, size means something else — a cols x rows pair, as in size="30x5" — matching the historical behaviour.
Label
A standalone label, outside any field:
<x-bf::label for="login">Login</x-bf::label>
echo BF::label('login', 'Login');
<label for="login">Login</label>
A standalone label emits its value as raw HTML, which makes a complex label easy to build.
BF::label() takes a fourth argument to escape it instead — a switch of its own, which the escape setting neither reads nor overrides.
Buttons & links
Four elements share the same shape. Their text is given as the first argument, or as the slot of the component; link takes its target first:
| Element | Renders | Default variant |
|---|---|---|
submit |
<input type="submit"> |
primary |
reset |
<input type="reset"> |
danger |
button |
<button type="button"> |
primary |
link |
an <a> styled as a button |
primary |
<x-bf::submit>Save</x-bf::submit>
<x-bf::link href="/users">Back</x-bf::link>
<input class="btn btn-primary" type="submit" value="Save">
<a href="https://example.com/users" class="btn btn-secondary">Back</a>
The second argument of the facade sets the Bootstrap variant, as a bare string.
A component has no such shortcut — its attributes always form an options array — so set the class instead, which replaces the default one entirely:
echo BF::button('Preview', 'outline-primary');
<x-bf::button class="btn btn-outline-primary">Preview</x-bf::button>
<button class="btn btn-outline-primary" type="button">Preview</button>
Configuration
Publishing the configuration file lands it at config/bootstrap_form.php:
php artisan vendor:publish --provider="Bgaze\BootstrapForm\BootstrapFormServiceProvider"
Every value it holds is a default, which a form or a field may override.
Root keys
| Key | Default | Effect |
|---|---|---|
blade_directives |
true |
Register the @open, @text, … Blade directives |
components |
true |
Register the bf x-component namespace. The facade and the directives remain available either way |
bootstrap_version |
5 |
The markup version, 4 or 5 |
layout |
vertical |
The default form layout |
group |
[] |
Application-wide HTML attributes of every form group. A class declared here replaces group_class; false drops the wrappers entirely |
show_all_errors |
false |
Render every error message of a field instead of only the first |
show_valid_feedback |
false |
Mark error-free fields as valid after a failed submit |
required_mark |
' *' |
The mark appended to the label of a required field. HTML is accepted verbatim, false disables it |
escape |
false |
Escape the content sinks — label, help, success, the addons, the Bootstrap 4 custom-file text — instead of emitting their value as raw HTML. See Content escaping |
Version sections
The layout-level options live under a bootstrap4 and a bootstrap5 section, and the active version decides which one applies.
Component classes — form-control, form-check, … — are driver code and are deliberately not configurable.
| Key | bootstrap4 |
bootstrap5 |
Effect |
|---|---|---|---|
custom |
false |
no-op | Use the Bootstrap 4 custom-styled controls by default |
group_class |
form-group |
mb-3 |
The default class of every form group, false for none |
left_class |
col-lg-2 col-xl-3 |
col-lg-2 col-xl-3 |
Horizontal layout: label column width |
right_class |
col-lg-10 col-xl-9 |
col-lg-10 col-xl-9 |
Horizontal layout: control column width |
pull_right |
hidden-md-down col-lg-2 col-xl-3 |
d-none d-lg-block col-lg-2 col-xl-3 |
Horizontal layout: spacer column for label-less fields |
lspace |
mr-2 |
me-2 |
Inline layout: space between a label and its control |
hspace |
mr-3 |
me-3 |
Inline layout: horizontal space between groups |
vspace |
my-1 |
my-1 |
Inline layout: vertical space between groups |
Laravel merges only the top level of a configuration file, so a published file replaces a whole version section.
The packaged defaults act as a floor under each one: a key you do not declare falls back to its default rather than to null, which is what keeps a file published before a key existed working.
Default group class
group_class is the only version-section key that cannot be overridden as a field setting — there is a single way to change it at a call site, and that is styling the group itself:
// This form and all its fields
BF::open(['group' => ['class' => 'mb-4']]);
// This field only
BF::text('login', null, null, ['group' => ['class' => 'mb-0']]);
// No class at all
BF::text('login', null, null, ['group' => ['class' => false]]);
| Option | Result |
|---|---|
| nothing | <div id="login-group" class="mb-3"> |
group:class="mb-4" |
<div class="mb-4" id="login-group"> |
group:class="false" |
<div id="login-group"> |
group="false" |
no wrapper at all, the control is rendered bare |
group:class="mb-4", horizontal layout |
<div class="mb-4 row" id="login-group"> — row belongs to the driver |
A supplied class replaces the default rather than adding to it, and takes over the inline spacing along with it.
Prefer setting the application-wide value here over repeating it at every call site:
<div id="login-group" class="mb-4">
<label for="login" class="form-label">Login</label>
<div>
<input id="login" class="form-control" name="login" type="text">
</div>
</div>
Available fields & syntaxes
Every field exists in the three syntaxes, and they render the same markup.
Unless stated otherwise a field takes (name, label, value, options), and an x-component exposes those arguments as attributes of the same name.
Forms
| Component | Facade method | Blade directive | Description |
|---|---|---|---|
<x-bf::form> |
BF::open() |
@open() |
Open a form, with the layout of the configuration |
| the component auto-closes | BF::close() |
@close |
Close a form |
vertical attribute |
BF::vertical() |
@vertical() |
Open a vertical form |
horizontal attribute |
BF::horizontal() |
@horizontal() |
Open a horizontal form |
inline attribute |
BF::inline() |
@inline() |
Open an inline form |
floating attribute |
BF::floating() |
@floating() |
Open a form with floating labels |
Text-like inputs
| Component | Facade method | Blade directive | Description |
|---|---|---|---|
<x-bf::text> |
BF::text() |
@text() |
A text input |
<x-bf::email> |
BF::email() |
@email() |
An email input, its name defaulting to email |
<x-bf::url> |
BF::url() |
@url() |
A URL input |
<x-bf::tel> |
BF::tel() |
@tel() |
A telephone input |
<x-bf::number> |
BF::number() |
@number() |
A number input |
<x-bf::date> |
BF::date() |
@date() |
A date input |
<x-bf::time> |
BF::time() |
@time() |
A time input |
<x-bf::datetime-local> |
BF::datetimeLocal() |
@datetimeLocal() |
A local date and time input |
<x-bf::month> |
BF::month() |
@month() |
A month input |
<x-bf::week> |
BF::week() |
@week() |
A week input |
<x-bf::search> |
BF::search() |
@search() |
A search input |
<x-bf::color> |
BF::color() |
@color() |
A color picker |
<x-bf::textarea> |
BF::textarea() |
@textarea() |
A textarea |
<x-bf::password> |
BF::password() |
@password() |
A password input — (name, label, options), no value |
Choice inputs
| Component | Facade method | Blade directive | Description |
|---|---|---|---|
<x-bf::select> |
BF::select() |
@select() |
A select — (name, label, choices, selected, options) |
<x-bf::checkbox> |
BF::checkbox() |
@checkbox() |
A single checkbox — (name, label, value, checked, options) |
<x-bf::checkboxes> |
BF::checkboxes() |
@checkboxes() |
A checkbox group — (name, label, choices, checked, options) |
<x-bf::radio> |
BF::radio() |
@radio() |
A single radio — (name, label, value, checked, options) |
<x-bf::radios> |
BF::radios() |
@radios() |
A radio group — (name, label, choices, checked, options) |
Other inputs
| Component | Facade method | Blade directive | Description |
|---|---|---|---|
<x-bf::file> |
BF::file() |
@file() |
A file input — (name, label, options), no value |
<x-bf::range> |
BF::range() |
@range() |
A range input |
<x-bf::hidden> |
BF::hidden() |
@hidden() |
A hidden input — (name, value, options), no label or group |
Buttons & elements
| Component | Facade method | Blade directive | Description |
|---|---|---|---|
<x-bf::label> |
BF::label() |
@label() |
A standalone label — (name, value, options) |
<x-bf::submit> |
BF::submit() |
@submit() |
A submit button — (value, options) |
<x-bf::reset> |
BF::reset() |
@reset() |
A reset button — (value, options) |
<x-bf::button> |
BF::button() |
@button() |
A plain button — (value, options) |
<x-bf::link> |
BF::link() |
@link() |
A link styled as a button — (url, title, options) |
Other packages
Feel free to visit my other packages:
bgaze/laravel-kvstore
Key-value store for Laravel 12 & 13
A database-backed key-value store for Laravel 12 & 13, sized for settings: read on most requests, written rarely.
The whole store is cached as one single entry, so a cold read costs one query for every key at once.
Each entry carries its own cast, so a value always reads back as what it was written as.
SnapStack
100% local browser captures for your AI assistant
SnapStack is a browser extension that captures any tab in one click and stacks it locally, so your AI assistant can read the screenshots on demand over MCP.
Nothing is ever uploaded: captures go only to a small server on your own machine. No account, no telemetry.
Works with any MCP-capable client (Claude Code and others), on Chrome, Edge and Firefox.
@bgaze/color-palette
A grid colour picker in the spirit of Google Docs
Vanilla TypeScript, with a single runtime dependency for the positioning.
It works standalone, ships optional Bootstrap 4 and Bootstrap 5 themes, and is accessible on purpose.