Start Here
Troubleshooting
The failures people actually hit — mostly configuration mismatches in the host application — and the fix for each.
On this page
- Buttons and inputs are invisible
- Components render unstyled
- Slide-over is anchored left, overflows, or won't scroll
- "No publishable resources for tag"
- Alpine errors or components behaving twice
- A nested table shows the outer table's rows
- A table control undoes itself on a polling table
- wireX is not defined after a wire:navigate visit
- JavaScript 404s and wireX is not defined
- Reordering stops working, or my own code loses window.Sortable
- A field's JavaScript does not run inside a modal
- save() throws with no model
- Notifications / toasts don't appear
- Validation errors don't display
- A tour never appears
- A tour I edited does not run again
- Still stuck?
Common issues and their fixes. Most are configuration mismatches in the host Laravel app rather than bugs in Wire.
Buttons and inputs are invisible
Symptom: Buttons, badges, and focus rings render as white-on-transparent or vanish entirely.
Cause: The primary Tailwind color is not defined. Wire uses primary for
every accent, so without it those elements have no color.
Fix: Define primary in your Tailwind config — see
Getting Started → Primary Color and
Theming → Colors.
Components render unstyled
Symptom: Markup appears but with no Tailwind styling.
Cause: Tailwind is not scanning the Wire vendor views, so their classes are purged from the build.
Fix: Add the Wire view paths to your Tailwind content array:
content: [ './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',],
The src/**/*.php paths matter: some components compose their utility classes
in PHP (positioning, width, height), so scanning only the Blade views leaves
those classes purged. On Tailwind 4, add the matching @source lines for both
resources/views and src (see the getting-started guide).
Rebuild assets afterward (npm run build or npm run dev).
Slide-over is anchored left, overflows, or won't scroll
Symptom: An action's slide-over (->slideOver()) appears pinned to the
left with the dimmed page on the right, its content overflows past the
viewport, the footer sits off-screen at the bottom, and the body won't
scroll.
Cause: The slide-over composes its positioning, width and height utilities
in PHP (SlideOverComponent), not in a Blade file — classes like sm:right-0,
sm:pl-10, sm:h-full, max-h-[85vh] and sm:max-w-2xl. If Tailwind only
scans resources/views and not the package src, those classes are purged:
without sm:right-0 the panel falls to the left, and without the height
utilities it is never height-constrained, so it overflows instead of scrolling
its body.
Fix: Add the src/**/*.php paths (Tailwind 3) or the matching @source
lines (Tailwind 4) as shown in Components render unstyled,
then rebuild. See also the getting-started guide.
"No publishable resources for tag"
Symptom: vendor:publish reports no resources for a tag like
wire-forms-config.
Cause: Wire's publish tags use a :: separator, not a dash.
Fix: Use the correct tag format:
php artisan vendor:publish --tag=wire-forms::config # ✅php artisan vendor:publish --tag=wire-forms::viewsphp artisan vendor:publish --tag=wire-forms::translations
The groups are config, views, translations, and (where applicable)
migrations — each prefixed with the package short name and ::.
Alpine errors or components behaving twice
Symptom: Console errors about Alpine being initialized twice, or directives firing twice.
Cause: Alpine was installed and started separately. Livewire 4 already ships and starts Alpine.
Fix: Remove any standalone Alpine install and Alpine.start() call. Let
Livewire provide it.
A nested table shows the outer table's rows
Symptom: On a page whose component has a table and also contains another
Livewire component with its own table (an order with its delivery notes in a
tab), sorting or changing the page size of the outer table does nothing to it,
while the nested table suddenly lists the outer table's rows. The console shows
isSelected is not defined and similar.
Cause: Livewire 4 applies an island fragment by searching the component's
DOM for the marker carrying the fragment's token — and it does not stop at a
nested component. The token names the compiled island view, so every table on
the page shares it (data-region, action-modals), and the outer table's
fragment lands in the inner one. Present in every Livewire 4.x up to 4.4.6
(fix proposed in livewire/livewire#10737).
Fix: Keep @wireStackScripts in the layout — wire-core 2.1.1 hides the
nested markers for the length of each message, so the fragment can only land in
its own component. A layout without the wire-core bundle does not get the
workaround.
A table control undoes itself on a polling table
Symptom: On a table with poll() or live(), changing the page size or
clicking a sort header now and then does nothing: the select jumps back to the
old value and the rows stay as they were. Doing it again works. Opening a modal
or saving a cell can lose its effect the same way.
Cause: The table's rows and modals are Livewire islands; the poll, the search box and the filters are not. Livewire 4 coordinates overlapping requests only within one scope, so a control fired inside the island while a poll tick is in flight goes out beside it with the same snapshot — and, as the Livewire islands documentation puts it, "the last response to return will win the state battle". When that is the tick, its older snapshot overwrites the change.
Fix: Keep @wireStackScripts in the layout. From wire-core 2.2.3 the table
renders data-wire-islands="shared-state" and wire-core applies Livewire's own
rules across that component's islands and its root: a tick arriving while
something else is in flight is dropped, a tick in flight is cancelled for a user
action, anything else waits for the other request. A table view published before
2.2.3 lacks the marker — re-publish it (--tag=wire-table::views --force). Your
own component with islands over shared state can opt in with the same attribute
on an element it owns.
wireX is not defined after a wire:navigate visit
Symptom: A page works when loaded directly, but reaching it through
wire:navigate — or with the browser's Back/Forward buttons — leaves the table
or form inert. The console shows a ReferenceError naming a Wire component
(wireRecordSelection, wireDropdown, wireSortable, …). The loudest version
is a full-page grey scrim over a dead table: every mobile sheet backdrop is bound
to state that was never created.
Cause: The bundle that defines the component arrived with the new page, and
Livewire's cached Back/Forward path does not wait for newly injected <head>
scripts before it initialises Alpine on the swapped-in markup.
Fix: Put the directive in your layout <head> so the controllers are in the
document from the first page load, where nothing can outrun them:
<head> @wireStackScripts</head>
See Getting Started → JavaScript Assets.
JavaScript 404s and wireX is not defined
Symptom: The same ReferenceError as the previous entry, but on every page
and however you reached it — a hard reload shows it too. The network tab has 404s on
/wire-core/assets/dropdown.js, /wire-table/assets/records.js, or a sibling
under /wire-forms/… or /wire-sortable/….
Cause: Two things went wrong together. The packages normally copy their bundles
into public/vendor/<package> and emit those paths, so nothing hits PHP — a
/wire-core/assets/… URL in your markup means that copy could not be made and the
package route is standing in for it. And your web server is answering the route
itself instead of forwarding it to PHP. The stock Laravel nginx config sends
anything not on disk to index.php, but a config with a static-asset block does not:
location ~* \.(js|css)$ { try_files $uri =404; # a route is not a file on disk → 404, PHP never sees it}
Nothing here is package-specific: the same block 404s Livewire's own
/livewire-{hash}/livewire.js.
Fix — make public/ writable, or write it at build time. The usual cause is a
public/ the web user cannot write to, or a read-only container. Either grant the
write, or do the copy while the filesystem still is writable:
php artisan vendor:publish --tag=laravel-assets --force
Or make the route reachable, by letting the block fall through to the front
controller — the right answer where a writable public/ is genuinely not on offer:
location ~* \.(js|css)$ { try_files $uri /index.php?$query_string; // [tl! focus]}
A related warning, when copies exist but could not be refreshed after an upgrade:
wireStack: the published copies of wire-core/dropdown are older than the bundlesthe packages ship, and are what this page just loaded.
The page still works — an old bundle beats no bundle — but the same writability problem is behind it. See Getting Started → JavaScript Assets.
Reordering stops working, or my own code loses window.Sortable
Symptom: After upgrading, your application's own JavaScript throws
Sortable is not defined.
Cause: SortableJS is now compiled into the wire-sortable bundle, and
config('wire-sortable.sortablejs_cdn') defaults to null — so the CDN script
that used to leave a global behind is no longer loaded. Wire's own reordering is
unaffected; the drag controller uses the bundled copy.
Fix: If your code needs the global, ask for it — either set the config key
back to a CDN URL, or npm install sortablejs and assign window.Sortable
yourself. See
Sortable Installation → SortableJS.
A field's JavaScript does not run inside a modal
Symptom: A JS-backed field works on a normal page but is dead when opened in a modal or after a Livewire update.
Cause: A plain <script> tag injected through Livewire's DOM morphing never
executes.
Fix: Load the script with Livewire's @assets directive (the built-in
TiptapEditor does this). If you are building a custom JS field, follow the same
pattern — see
Extending Forms → JS-Backed Fields.
save() throws with no model
Symptom: Calling $form->save() throws.
Cause: The form has no model (model(null) or none set), so there is nothing
to persist.
Fix: Either set a model (->model(User::class) to create,
->model($user) to update), provide custom persistence with
->using(...), or call ->validate() instead when you only need the data. See
Model Modes.
Notifications / toasts don't appear
Symptom: Actions succeed but no toast or notification shows.
Cause: The notifications container is missing from the layout.
Fix: Add it once near the end of <body>:
<x-wire-notifications::toast-container />
See the layout in Getting Started and Core → Notifications.
Validation errors don't display
Symptom: Validation fails (the save is blocked) but no message renders under the field.
Cause: The form's statePath does not match the component property holding
the state, so error keys and field paths diverge.
Fix: Ensure ->statePath('data') matches a public property (public array $data = []) and that you render the form with {{ $this->form }}. Field errors
are keyed by the full state path (for example data.email) — assert on that path
in tests.
A tour never appears
Symptom: A tour is registered and no walkthrough runs.
Causes, in the order worth checking — a tour that does not run is always silent, because every one of these is a legitimate thing for it to decide:
- This person has already seen it. Finishing and skipping both count, and
the record is kept for the tour's current
since()value. In development you meet this after the very first page load. Use Replay the tour in the user menu, or setWIRE_TOURS_DRIVER=nullin.envwhile you work on it, which makes it run on every page load. - They put it off. A tour with a welcome block
can be answered with "Later", which silences it for the rest of that session
— and the "Later" that spends the tour's
postpone()allowance records it as seen for good, exactly as skipping would. Signing in again, or thenulldriver, clears it. - Nothing draws it. The panel is rendered through
PageChrome, whichwire-admin's layout renders. A layout of your own has to render the same two loops — see Tour → Your own layout.@wireStackScriptsmust be in the<head>too, because the panel is positioned by the controllers it delivers. - Every step's element is missing or hidden. A step whose element is not on the page, or is rendered but has no size (a dropdown that is closed, a bulk bar before rows are selected), is skipped on purpose — and a tour that loses all of its steps this way does not start. Check the names against the page with devtools; see Theming → Finding a name.
- The screen does not match.
zones(),resource()andpage()combine with AND.zones('sales')never matches an application with no zones —zones(null)is the unzoned one. - Authorization denies it.
permission(),authorize()andauthorizeUsing()fail closed, and a tour with a permission is never shown to a guest. - Another tour claimed the screen first. Only one runs per visit; the
lowest
sort()wins and the rest wait for a later visit.
Fix: work down that list against
Tour → How It Works. To prove the wiring
without touching any of the scoping, register a tour with no constraints at all
and one step pointing at admin-sidebar: if that does not run, the cause is (1),
(2) or (3).
A tour I edited does not run again
Symptom: The steps or wording changed, the deploy went out, and people who had already finished the tour still see nothing.
Cause: Nothing hashes a tour's content and nothing reads a package version.
What is stored against a person is the tour's since() value, and it is
compared for inequality — so an edited tour whose since() did not move is
still acknowledged.
Fix: change since() to anything different — a release number, a date, a
word:
Tour::make('getting-started')->since('2.2')->steps([...]);
Everybody who finished the old version sees the new one once. Nobody sees it
twice, and there is no store to clear and no command to run. Renaming the tour's
id has the same visible effect, but it is a different thing: the id is what
acknowledgements are stored against, so a rename throws away the record for
everybody and for good. Change since(), not the id. See
Tour → What's New After an Upgrade.
Still stuck?
- Re-read the package-specific doc for the feature (Forms, Table, Sortable, Core).
- For runtime/state issues, run the Integration suite to see if the behavior is
reproduced:
vendor/bin/pest --configuration phpunit.xml --testsuite "Integration". - Check the Upgrade guide if the problem started after updating.