rishadblack / wire-bootstrap
Livewire-native Bootstrap 5.3 UI: one artisan command sets up Bootstrap, Vite and a base layout, and every form field is a single Blade tag with its own label, wire:model binding, validation errors and loading states.
Requires
- php: ^8.3
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/filesystem: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- illuminate/view: ^11.0|^12.0|^13.0
- livewire/livewire: ^3.0|^4.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.20
- livewire/blaze: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.8|^4.0|^5.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0|^5.0
Suggests
- livewire/blaze: Compiles the wire-bootstrap components to plain PHP functions for 90%+ faster rendering (auto-detected).
Provides
None
Conflicts
None
Replaces
None
README
Livewire-native Bootstrap 5.3 UI: one artisan command sets up Bootstrap, Vite and a base layout in a fresh Laravel app, and every form field is a single Blade tag that brings its own label,
wire:modelbinding, ids, validation errors and loading states — restyle it per project with SCSS variables and published views, without owning the plumbing.
<x-wb::input.text label="Name" wire:model="name" required />
renders the label with a required marker, derives the input id and name from wire:model, shows the
field's validation error with Bootstrap's is-invalid / invalid-feedback styling, and builds a
placeholder from the label — ten lines of Bootstrap form markup in one tag.
Contents
- Requirements
- Installation
- What the installer does
- Manual installation
- Your first form
- Field reference
- Components
- Layout and dark mode
- Performance (Blaze)
- Configuration reference
- Theming
- Extending
- Sibling packages
- Testing
- Troubleshooting
- License
Requirements
| Dependency | Version |
|---|---|
| PHP | 8.3+ |
| Laravel | 11, 12 or 13 |
| Livewire | 3 or 4 |
| Bootstrap | 5.3 (installed for you by the installer) |
| livewire/blaze | optional, auto-detected (90%+ faster component rendering) |
Installation
composer require rishadblack/wire-bootstrap php artisan wire-bootstrap:install
That is the whole setup for a fresh Laravel + Livewire app. Flags:
| Flag | Effect |
|---|---|
--no-npm |
edit package.json but do not run npm install |
--skip-build |
do not run npm run build at the end |
--no-layout |
do not publish the app layout |
--no-blaze |
do not composer require livewire/blaze |
--force |
overwrite files a previous run published |
The command is idempotent: a second run changes nothing, and files it cannot safely edit are left untouched with the manual snippet printed instead.
What the installer does
- Adds
bootstrap,@popperjs/core,flatpickrandquill(andsassas a dev dependency) topackage.jsonwithout touching existing entries, then runsnpm install. - Writes
resources/scss/app.scss: a documented variable-override section, the Bootstrap source import, and the styles the installedwire-*packages need. - Writes
resources/js/wire-bootstrap-setup.js(managed — regenerated on re-runs): exposeswindow.bootstrapandwindow.flatpickr, imports this package's Alpine components and, when installed, the wire-tomselect and wire-modals scripts. A marker block inresources/js/app.jsimports it. - Extends
vite.config.js: adds the SCSS entry to thelaravel()input and silences dart-sass's@importdeprecation (Bootstrap 5.3 still uses@import; Bootstrap 6 moves to@use). - Publishes the app layout to wherever your Livewire config points (
component_layouton Livewire 4,layouton Livewire 3) — a thin wrapper around<x-wire-bootstrap::layout>. - Publishes
config/wire-bootstrap.phpand offerslivewire/blaze.
Manual installation
Already have Bootstrap 5.3 on window.bootstrap? Skip the installer entirely — the components only need
their Alpine layer:
// after the import that sets window.bootstrap import flatpickr from 'flatpickr'; window.flatpickr = flatpickr; // only needed for <x-wb::input.date> import Quill from 'quill'; window.Quill = Quill; // only needed for <x-wb::input.editor> import '../../vendor/rishadblack/wire-bootstrap/resources/js/wire-bootstrap.js';
Your first form
<form wire:submit="save"> <x-wb::card title="Profile"> <x-wb::input.text label="Name" wire:model="name" required /> <x-wb::input.email label="Email" wire:model="email" required prepend="@" /> <x-wb::input.select label="Country" wire:model="country" :options="['bd' => 'Bangladesh', 'fi' => 'Finland']" /> <x-wb::input.date label="Joined at" wire:model="joined_at" :max-date="now()->format('Y-m-d')" /> <x-wb::input.checkbox label="I agree to the terms" wire:model="agree" required /> <x-slot:footer> <x-wb::button type="submit" loading="save">Save</x-wb::button> </x-slot:footer> </x-wb::card> </form>
$this->validate() errors appear under each field automatically; the save button disables itself and
shows a spinner while the request runs. Every component is available under both namespaces:
<x-wire-bootstrap::input.text> and the configurable short alias <x-wb::input.text>.
Field reference
Props shared by every field:
| Prop | Default | Meaning |
|---|---|---|
label |
— | label text; no label element when omitted |
name / id |
derived from wire:model |
dotted models get a sanitized, collision-free id |
placeholder |
Type {label} |
pass placeholder="" to suppress |
required / optional |
false | marker next to the label ((*) / (Optional), from the lang file) + aria-required |
help |
— | muted form-text under the field |
size |
config | sm or lg |
floating |
config | Bootstrap floating label (text-like fields) |
wrapper-class |
— | classes for the outer <div> |
prepend / append |
— | input group; plain text is wrapped in input-group-text, markup (slot) is passed through |
Note on required: the native HTML attribute would let the browser block the submit before Livewire's
server-side validation runs, so fields render aria-required="true" and the visual marker instead —
validate in the component.
Extra props per field:
| Field | Extra props |
|---|---|
input.text |
type, datalist (array) |
input.email, input.number, input.hidden |
thin variants of text (min/max/step pass through as attributes) |
input.password |
toggle (eye button, default on) |
input.textarea |
rows |
input.select |
options (key => label or [['value' => ..., 'name' => ...]]), <option> slot, multiple (drops the placeholder, appends [] to the name) |
input.select-search |
field chrome around a custom control in the slot (wire-tomselect), plain select fallback without one |
input.checkbox / input.switch |
value, inline |
input.radio / input.checkbox-group |
options, inline; one error block for the group |
input.date |
date, time, format, min-date, max-date, inline, default-date, enable-dates, disable-dates, range |
input.editor |
rich text (Quill 2, snow theme): toolbar (basic, full or a Quill toolbar array), height, read-only; binds HTML through a hidden input — sanitize before echoing with {!! !!} |
input.file |
multiple, accept; Livewire upload progress bar built in |
input.color, input.range |
range: min, max, step, show-value |
Any other attribute (wire:model modifiers, data-*, maxlength, disabled, readonly, …) passes
through to the control element, with class merged.
Dates
input.date mounts flatpickr behind wire:ignore and keeps wire:model in sync. Dates you pass
(min-date, enable-dates, …) may be Y-m-d, d-M-Y, d-m-Y or anything strtotime understands.
Pass ['disable_all'] as enable-dates to disable every date. Update enabled dates at runtime from the
component:
$this->dispatch('wire-bootstrap:date-options', model: 'joined_at', enable: ['2026-06-25', '2026-06-26']);
Components
button—variant(default from config),outline,size,icon(class string),type(defaultbutton),loading(awire:target). Withwire:clickorloading, the button disables itself and swaps in a spinner while the action runs.card—title,toolbarslot (right side of the header),footerslot,body-class.tabs/tab—:items="['key' => 'Title']"generates the nav (or use thenavslot);<x-wb::tab key="..." active>panes; Alpine-driven, and inside a wire-modals modal the tabs reset to the default when the modal reopens (:reset-on-modal-open="false"to keep).badge—variant,pill.alert—variant,dismissible(Bootstrap'sdata-bs-dismiss, no JS needed).empty-state—title,description, action slot.theme-toggle— light/dark button (with-autoadds an auto step), persists inlocalStorage.layout— see below.
Layout and dark mode
<x-wire-bootstrap::layout :title="$title ?? null" class="bg-body-tertiary"> <x-slot:nav><nav>…your navbar…</nav></x-slot:nav> {{ $slot }} <x-slot:footer>…</x-slot:footer> </x-wire-bootstrap::layout>
The layout renders the HTML skeleton: meta, <title>, @vite (entries from config), @stack('css') /
@stack('js'), head and scripts slots, and body attributes pass through. It ships no navbar or
sidebar — navigation is your app's.
With theme.dark_mode on, a tiny pre-paint snippet applies the visitor's stored data-bs-theme before
first paint (no flash). That snippet is the package's single inline script; under a strict CSP, publish
the view and add your nonce to it, or disable dark mode.
Performance (Blaze)
Install livewire/blaze (the installer offers it) and wire-bootstrap
automatically opts its component views into Blaze's function compiler — Blade component overhead drops by
90%+ on component-heavy pages. No configuration; set wire-bootstrap.blaze => false to opt out. Every
component here is an anonymous component specifically so Blaze can compile it. Verify with
Blaze::debug(). Opt your own components in with Blaze::optimize()->in(resource_path('views/components')).
Configuration reference
php artisan vendor:publish --tag=wire-bootstrap-config
| Key | Default | Meaning |
|---|---|---|
alias_prefix |
wb |
second component namespace; null disables |
vite_entries |
scss + js | what the layout passes to @vite |
defaults.size |
null |
global field size (sm / lg) |
defaults.floating |
false |
floating labels by default |
defaults.button_variant |
primary |
|
defaults.show_placeholder |
true |
auto Type {label} placeholders |
markers.required / markers.optional |
true |
label markers |
date.display_format / display_format_time |
d-M-Y / d-M-Y h:i K |
flatpickr display formats |
date.flatpickr |
[] |
extra options merged into every picker |
theme.dark_mode |
true |
theme toggle + pre-paint snippet |
theme.default |
light |
light / dark / auto |
blaze |
true |
auto-opt views into Blaze when installed |
Theming
- SCSS variables — the installer's
resources/scss/app.scsshas an overrides section above the Bootstrap import; everything in the package is plain Bootstrap 5.3 classes and CSS variables, so your overrides restyle it all. - Lang lines —
php artisan vendor:publish --tag=wire-bootstrap-langto change the markers, placeholder pattern and loading text (or translate them). - Views —
php artisan vendor:publish --tag=wire-bootstrap-viewsfor full markup control.
Extending
A new field is one Blade file reusing the foundation:
{{-- resources/views/components/input/slug.blade.php (your app) --}} @props(['label' => null, 'name' => null, 'id' => null, 'required' => false, 'optional' => false, 'help' => null, 'size' => null]) @php($field = \Rishadblack\WireBootstrap\Support\FieldState::resolve($attributes, [ 'label' => $label, 'name' => $name, 'id' => $id, 'required' => $required, 'optional' => $optional, 'help' => $help, 'size' => $size, 'floating' => false, ], $__env)) <x-wire-bootstrap::field :field="$field"> <input type="text" id="{{ $field->id }}" name="{{ $field->name }}" pattern="[a-z0-9-]+" {{ $attributes->class(['form-control', 'is-invalid' => $field->invalid]) }} /> </x-wire-bootstrap::field>
FieldState gives you the derived id/name, the error state, markers and placeholder;
<x-wire-bootstrap::field> renders the chrome.
Sibling packages
The installer detects and wires these automatically:
- rishadblack/wire-tomselect — searchable dropdowns;
pair with
<x-wb::input.select-search>for the field chrome. - rishadblack/wire-modals — on-demand Livewire components in Bootstrap modals; tabs inside a modal reset when it reopens.
- rishadblack/i-reports — paginated, exportable reports in the same Bootstrap 5.3 look.
Testing
it('shows the validation error', function () { Livewire::test(ProfileForm::class) ->call('save') ->assertHasErrors('email') ->assertSee('is-invalid', false); });
Fields are plain Blade components — Blade::render('<x-wb::input.text ... />') works for markup
assertions, and Livewire::test() covers binding and validation.
Troubleshooting
- "wbDatepicker is not defined" / date field is a plain input — the package JS is not loaded, or
flatpickr is missing from the bundle. Re-run
wire-bootstrap:install, thennpm run build. - Sass deprecation warnings about
@import— expected with Bootstrap 5.3; the installer silences them viacss.preprocessorOptions.scssinvite.config.js. Re-run the installer or copy that block. - Submit does nothing — make sure the form uses
wire:submitand the buttontype="submit". The fields themselves never block submission (no nativerequired). - Errors not showing — the error key is the
wire:modelpath; with a custom control andinput.select-search, passname="your_property"so the chrome knows which error to show. - Dark mode flashes or is stuck — the stored theme lives in
localStorageunderwb-theme; the pre-paint snippet only renders whentheme.dark_modeis on.
License
MIT. See license.md.