Skip to content

Install

Terminal window
composer require matheusmarnt/ghostwire

Publish the config (optional — sensible defaults ship without it):

Terminal window
php artisan vendor:publish --tag=ghostwire-config

Publish the compiled JS/CSS assets — without this step the browser gets a 404 for ghostwire.js/ghostwire.css and wire:ghost never fires:

Terminal window
php artisan vendor:publish --tag=ghostwire-assets

In a local environment, Ghostwire publishes these assets automatically at boot if they’re missing, so this manual step mainly matters for staging/production.

Add both directives to your main layout, once — usually right next to @livewireStyles/@livewireScripts:

{{-- in <head> --}}
@ghostwireStyles
{{-- right before </body> --}}
@ghostwireScripts

php artisan ghost:install runs the publish step above and prints this same reminder, in one command. Run php artisan ghost:install --force after a package upgrade to republish updated assets — without --force, existing published files are left alone.

Add the directive to any element inside a Livewire component:

<div wire:ghost>
<h2>{{ $title }}</h2>
<p>{{ $body }}</p>
</div>

Trigger any Livewire update on that component (an action call, wire:model.live, etc.) — Ghostwire synthesizes a matching skeleton from the live DOM automatically, no further markup required. First visible effect: under 60 seconds from composer require.

Don’t want the loading strategy applied everywhere? config/ghostwire.php’s strategy key defaults to 'opt-in': a component is instrumented when #[Ghost] is declared on it, on an ancestor, on a trait it uses, or on one of its action methods — or when any element in its view carries wire:ghost — or when strategy is set to 'global'. Flip strategy to 'global' to apply the loading strategy to every Livewire component, opt-out per component with wire:ghost.off or #[Ghost(mode: 'off')].

Want every #[Ghost]-opted-in component to emit panel bones by default, without declaring #[Ghost(panels: true)] on each one? Set GHOSTWIRE_PANELS=true (or config/ghostwire.php’s panels key) — it defaults to false. A class-level #[Ghost(panels: ...)] still overrides this app-wide default per component, same precedence as mode/delay/hold. See #[Ghost] attribute for what panels does.

Set GHOSTWIRE_ENABLED=false (it defaults to true) to disable Ghostwire entirely — no hook registration, no data-ghost payload, no lazy-placeholder tagging, and @ghostwireStyles/@ghostwireScripts both emit nothing. If config/ghostwire.php has been cached (php artisan config:cache), changing this env var only takes effect after php artisan config:clear (or a fresh cache).

Ghostwire ships three distinct loading mechanisms that are easy to conflate — each covers a different moment in a page’s lifecycle:

MechanismCovers
wire:ghost / #[Ghost]A Livewire request on an already-loaded page (an action call, wire:model.live, polling)
#[Lazy] + #[Ghost(lazy: true)] + learningThe initial page load / a browser refresh, in development — a locally learned skeleton paints into the lazy placeholder
ghost:export + placeholder()The initial page load / a browser refresh, in production — a static, server-side Blade placeholder, no JS or localStorage involved

None of the three substitute for another: wire:ghost alone does nothing on a full page load (there is no Livewire request or DOM morph to intercept), and learning never runs in production regardless of config. See /docs/learning for the full walkthrough of the second and third mechanisms.