Start Here
Getting Started
This guide covers the production setup for Wire in a Laravel application.
On this page
- Requirements
- Installation
- Full ecosystem (table + forms + core)
- Only forms (forms + core)
- Only core
- Sortable package (drag and drop row reordering)
- Production Checklist
- Tailwind CSS Configuration
- Primary Color
- Layout Template
- JavaScript Assets
- Why an SPA app wants it
- Where the files actually come from
- If public/ is not writable
- Config Publishing (optional)
- View Publishing (optional)
- Quick Start: Table
- Quick Start: Form
- Troubleshooting
- Styles are missing
- Components render without JavaScript behavior
- Notifications do not appear
- Development (monorepo)
- Next Steps
This guide covers the production setup for Wire in a Laravel application.
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | 12.61+ or 13.12+ |
| Livewire | 3.x |
| Tailwind CSS | 3.x+ |
| Alpine.js | 3.x+ (included with Livewire) |
nyoncode/laravel-package-toolkit |
^2.4 (installed for you) |
The last row is not something you require yourself — Composer pulls it in with the
Wire packages. It is listed because it decides the two rows above it. The toolkit
owns the public/vendor mirror that puts the JavaScript bundles on disk (see
JavaScript Assets), and 2.4 requires
illuminate/support ^12.61.1|^13.12.0, so that — not the ^12.0 the Wire packages
declare — is the Laravel version an install actually resolves against. An app that
pins the toolkit itself has to allow ^2.4 before composer require nyoncode/wire-table
can resolve at all.
Installation
Full ecosystem (table + forms + core)
composer require nyoncode/wire-table
Only forms (forms + core)
composer require nyoncode/wire-forms
Only core
composer require nyoncode/wire-core
Sortable package (drag and drop row reordering)
composer require nyoncode/wire-sortable
Service providers register automatically via Laravel auto-discovery.
Production Checklist
Before you render the first component, make sure all of these are true:
- Livewire 3 is installed
- Tailwind scans the Wire vendor views
- your app defines a
primarycolor - the main layout includes
@vite,@livewireStyles, and@livewireScripts - the layout has
@wireStackScriptsin its<head>— required in practice for any app that navigates withwire:navigate(see JavaScript Assets) - the layout renders
<x-wire-notifications::toast-container />if you want built-in toasts
Tailwind CSS Configuration
Wire generates some utility classes from PHP (color/size resolvers, the mobile
bottom-sheet classes, responsive grid columns, …), so Tailwind must scan both
the package views and src — scanning views alone will miss those classes.
Tailwind 3 — add the paths to tailwind.config.js:
module.exports = { content: [ // ...your paths './vendor/nyoncode/wire-core/resources/views/**/*.blade.php', './vendor/nyoncode/wire-core/src/**/*.php', './vendor/nyoncode/wire-forms/resources/views/**/*.blade.php', './vendor/nyoncode/wire-forms/src/**/*.php', './vendor/nyoncode/wire-table/resources/views/**/*.blade.php', './vendor/nyoncode/wire-table/src/**/*.php', './vendor/nyoncode/wire-sortable/resources/views/**/*.blade.php', './vendor/nyoncode/wire-sortable/src/**/*.php', ],}
Tailwind 4 — add @source lines to your CSS entry (e.g. app.css):
@source "../../vendor/nyoncode/wire-core/resources/views";@source "../../vendor/nyoncode/wire-core/src";@source "../../vendor/nyoncode/wire-forms/resources/views";@source "../../vendor/nyoncode/wire-forms/src";@source "../../vendor/nyoncode/wire-table/resources/views";@source "../../vendor/nyoncode/wire-table/src";@source "../../vendor/nyoncode/wire-sortable/resources/views";@source "../../vendor/nyoncode/wire-sortable/src";
Primary Color
Wire components use primary as the default accent color (buttons, badges, focus rings, etc.). You must define it in your Tailwind config:
Tailwind 3 (tailwind.config.js):
const colors = require('tailwindcss/colors') module.exports = { theme: { extend: { colors: { primary: colors.blue, // or any color palette }, }, },}
Tailwind 4 (app.css):
@theme { --color-primary-50: var(--color-blue-50); --color-primary-100: var(--color-blue-100); --color-primary-200: var(--color-blue-200); --color-primary-300: var(--color-blue-300); --color-primary-400: var(--color-blue-400); --color-primary-500: var(--color-blue-500); --color-primary-600: var(--color-blue-600); --color-primary-700: var(--color-blue-700); --color-primary-800: var(--color-blue-800); --color-primary-900: var(--color-blue-900); --color-primary-950: var(--color-blue-950);}
Without a
primarycolor defined, buttons and other interactive elements will be invisible (white text on a transparent background).
Layout Template
Your main layout must include Vite assets and Livewire, plus one @wireStackScripts
in the <head>. Add the notifications container if you use action feedback or toasts.
<!DOCTYPE html><html lang="{{ str_replace('_', '-', app()->getLocale()) }}"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> @vite(['resources/css/app.css', 'resources/js/app.js']) @livewireStyles @wireStackScripts </head><body> {{ $slot }} <x-wire-notifications::toast-container /> @livewireScripts</body></html>
Do not install Alpine separately. Livewire 3 already ships it.
JavaScript Assets
Wire's interactive parts — dropdowns, the row context menu, tabs, wizards,
inline-edit cells, the fill handle, row selection, record actions, drag & drop
reordering — are small Alpine components delivered as pre-built bundles from
inside the packages. There is nothing to install, nothing to publish and no build
step on your side: the packages copy their bundles into public/vendor/ themselves
and serve them as static files, cache-busted by the file's modification time.
@wireStackScripts puts every installed package's bundles in the document.
One line in the layout <head>, and every controller is present on every page:
<head> @wireStackScripts</head>
Narrow it to a single package if you want to:
@wireStackScripts('wire-table')
Why an SPA app wants it
Without the directive, each surface still loads its own bundle when it renders — so a table page loads the table bundles, a form page the form ones. That works, and an app that never adds the directive keeps working.
It stops being enough as soon as you navigate with wire:navigate. Livewire's
cached Back/Forward path does not wait for newly injected <head> scripts
before it initialises Alpine on the swapped-in page. A bundle that arrives with
the new page can therefore lose the race, and the markup is initialised against a
registry that does not have the component yet:
Uncaught ReferenceError: wireRecordSelection is not defined
which shows up as dead dropdowns, checkboxes that do nothing and — the loudest symptom — a full-page grey scrim over the table, because every mobile sheet backdrop is bound to state that no longer exists.
A bundle that was already in the document when the visitor first arrived cannot
lose that race. That is the whole job of @wireStackScripts: it is not a
convenience, it is the only placement the cached back/forward path cannot beat.
Heavy, optional assets — the TipTap rich editor, the Chart.js chart controller — are deliberately not in the always-loaded set. The surface that needs one fetches it when it renders. Charts additionally need Chart.js, which stays your app's own dependency.
Where the files actually come from
They are real files under public/vendor/<package>, and they get there on
their own. The first page render after a deploy copies each package's bundles out
of the installed package and into public/, then emits those paths:
<script src="/vendor/wire-core/wire-core-dropdown.js?id=1786118403" ...></script>
Nothing to run, nothing to configure. The copy is incremental — a file already
present and current is left alone — so in steady state a request does a handful of
stat calls and no writes at all. After an upgrade it is one copy per changed
bundle, on one request. Copies land through a temporary file and an atomic rename,
so a browser fetching a bundle mid-copy never receives a half-written one.
This matters more than it sounds. Serving the bundles from a package route only
works if the request reaches PHP, and a very common web-server layout answers .js
itself:
location ~* \.(js|css)$ { try_files $uri =404; # a route is not a file on disk → 404}
On shared hosting that block is frequently not yours to change — and it breaks
Livewire's own /livewire/livewire.js in exactly the same way. A file that exists
is served by every web server configuration there is, so that is what the packages
ship you.
Publishing is still supported and does the same copy ahead of time, which moves it off the first request after a deploy:
php artisan vendor:publish --tag=laravel-assets --force
laravel-assets is the tag the Laravel skeleton already runs from its composer
post-update-cmd, so composer update keeps the copies current on its own. Neither
the command nor the hook is required.
If public/ is not writable
A read-only container, Vapor, a hardened deployment: nothing throws. The package
route serves the bundles exactly as it did before, and you want either the publish
command above (run at build time, when the filesystem still is writable) or the
try_files … /index.php?$query_string fall-through so the route is reachable.
If an older copy is already there, it keeps being served rather than falling back
to a route that may be unreachable — and the console says so, on every page and
regardless of APP_DEBUG, naming the bundles and the command that fixes them. See
Troubleshooting.
Config Publishing (optional)
php artisan vendor:publish --tag=wire-core::configphp artisan vendor:publish --tag=wire-forms::configphp artisan vendor:publish --tag=wire-table::configphp artisan vendor:publish --tag=wire-sortable::config
View Publishing (optional)
php artisan vendor:publish --tag=wire-core::viewsphp artisan vendor:publish --tag=wire-forms::viewsphp artisan vendor:publish --tag=wire-table::viewsphp artisan vendor:publish --tag=wire-sortable::views
Quick Start: Table
use Livewire\Component;use NyonCode\WireTable\Concerns\WithTable;use NyonCode\WireTable\Table;use NyonCode\WireTable\Columns\TextColumn;use NyonCode\WireTable\Columns\BadgeColumn;use NyonCode\WireTable\Filters\SelectFilter;use NyonCode\WireCore\Actions\Action;use NyonCode\WireCore\Actions\DeleteAction;use NyonCode\WireCore\Actions\DeleteBulkAction; class UserTable extends Component{ use WithTable; public function table(Table $table): Table { return $table ->model(User::class) ->columns([ TextColumn::make('name') ->sortable() ->searchable(), TextColumn::make('email') ->searchable(), BadgeColumn::make('role') ->colors([ 'admin' => 'primary', 'editor' => 'success', 'viewer' => 'gray', ]), TextColumn::make('created_at') ->dateTime('d.m.Y') ->sortable(), ]) ->filters([ SelectFilter::make('role') ->options([ 'admin' => 'Admin', 'editor' => 'Editor', 'viewer' => 'Viewer', ]), ]) ->actions([ Action::make('edit') ->icon('pencil') ->url(fn (User $r) => route('users.edit', $r)), DeleteAction::make(), ]) ->bulkActions([ DeleteBulkAction::make(), ]) ->defaultSort('name') ->searchable() ->paginated(); }}
<div> {{ $this->table }}</div>
Next: Columns, Filters, Actions
Quick Start: Form
use Livewire\Component;use NyonCode\WireForms\Forms\Form;use NyonCode\WireForms\Forms\WithForms;use NyonCode\WireForms\Components\TextInput;use NyonCode\WireForms\Components\Select;use NyonCode\WireForms\Components\Toggle; class EditUser extends Component{ use WithForms; public array $data = []; public function mount(User $user): void { $this->form()->model($user)->fill($user->toArray()); } public function form(Form $form): Form { return $form ->statePath('data') ->model(User::class) ->schema([ TextInput::make('name')->required()->maxLength(255), TextInput::make('email')->email()->required(), Select::make('role') ->options(['admin' => 'Admin', 'editor' => 'Editor', 'viewer' => 'Viewer']) ->required(), Toggle::make('active'), ]) ->successMessage('User saved.'); } public function save(): void { $this->form()->save(); }}
<form wire:submit="save"> {{ $this->form }} <button type="submit">Save</button></form>
Next: Field Reference, Validation, Save Lifecycle
Troubleshooting
Styles are missing
- verify the Wire vendor paths are present in Tailwind content or
@source - rebuild assets with
npm run build - clear compiled views with
php artisan view:clear
Components render without JavaScript behavior
- confirm the layout includes
@livewireScripts - confirm the layout
<head>includes@wireStackScripts— especially if the breakage only appears after awire:navigatevisit or on Back/Forward - remove any standalone Alpine bootstrap from
resources/js/app.js
Notifications do not appear
- confirm the layout renders
<x-wire-notifications::toast-container /> - verify your configured notification driver is valid
- check whether the action actually sends a success or failure notification
Development (monorepo)
git clone ...composer install # Run all testscomposer test # Per-packagecomposer test:core # 793 testscomposer test:forms # 212 testscomposer test:table # 369 testscomposer test:sortable # 10 tests # Code stylecomposer lint # Pint (Laravel preset) # Static analysiscomposer analyse # PHPStan level 6
Next Steps
- Table columns — all 13 column types
- Form fields — all field types and Form API
- Actions — row, bulk, header actions
- Core plugins — reusable app and package extensions
- Configuration — config files and environment variables
- Authorization — Gates, policies, permissions
- Table exports — CSV, Excel, PDF downloads
- Audit log — model change history
- Sortable rows — drag & drop row reordering