Utsav Somaiya / Article
Stop Click Hijacking: Mastering wire:navigate with Alpine Dialogs
A quick breakdown of how a simple dialog button kept triggering Livewire navigation and the clean fix using programmatic Livewire.navigate(). No hacks. Just reading the docs.
You build a card-style CTA. It’s clickable. It should navigate when tapped.
Inside that card, you also add a tiny button to open a dialog.
Here’s the thing: because the whole card is wrapped in a navigation click, that little button doesn’t stand a chance. Users try to open the dialog… and boom. they’re already on the next page.
I didn’t go hunting strangers or asking every developer forum to bless me. I opened the docs and figured it out.
Livewire Navigation Docs
Before: The Unwanted Navigation
Typical structure:
1<a 2 wire:navigate 3 href="{{ route('target.route', ['param' => $param->id]) }}" 4 x-data="{ dialogOpen: false }" 5 class="block rounded-2xl border border-zinc-200 p-5" 6> 7 <div class="flex items-start justify-between gap-4"> 8 <div> 9 <h3 class="text-lg font-semibold">Action Title</h3>10 <p class="mt-1 text-sm text-zinc-600">11 Tap anywhere on the card to continue.12 </p>13 </div>14 15 <button16 type="button"17 x-on:click="dialogOpen = true"18 class="inline-flex h-9 w-9 items-center justify-center rounded-full border"19 >20 <x-lucide-info class="h-4 w-4" />21 </button>22 </div>23 24 <div x-show="dialogOpen" class="dialog-panel">25 <p>Extra context for the user.</p>26 </div>27</a> 1<a 2 wire:navigate 3 href="{{ route('target.route', ['param' => $param->id]) }}" 4 x-data="{ dialogOpen: false }" 5 class="block rounded-2xl border border-zinc-200 p-5" 6> 7 <div class="flex items-start justify-between gap-4"> 8 <div> 9 <h3 class="text-lg font-semibold">Action Title</h3>10 <p class="mt-1 text-sm text-zinc-600">11 Tap anywhere on the card to continue.12 </p>13 </div>14 15 <button16 type="button"17 x-on:click="dialogOpen = true"18 class="inline-flex h-9 w-9 items-center justify-center rounded-full border"19 >20 <x-lucide-info class="h-4 w-4" />21 </button>22 </div>23 24 <div x-show="dialogOpen" class="dialog-panel">25 <p>Extra context for the user.</p>26 </div>27</a>Everything looks correct… but a button inside a clickable navigation wrapper = accidental page changes.
You click the info button ➜ navigation fires anyway
because the click event bubbles up to <a wire:navigate>.
After: Full Control Navigation Only When You Decide
Instead of letting wire:navigate automatically trigger,
we call it manually using Livewire.navigate().
1+ @script 2+ <script> 3+ window.redirectToTarget = () => { 4+ Livewire.navigate(@js(route('target.route', ['param' => $param->id]))) 5+ } 6+ </script> 7+ @endscript 8 9- <div10- x-data="{ dialogOpen: false }"11+ x-on:click="redirectToTarget"12- class="clickable-card"13- >14- <h3>15- Action Title16- <button17- type="button"18- x-on:click.stop.prevent="dialogOpen = true"19- >20- <x-lucide-info />21- </button>22- </h3>23-24- <p>Short description text</p>25-26- <div x-show="dialogOpen" class="dialog-panel">27- <h2>Dialog Title</h2>28- <p>More info goes here.</p>29- <button type="button" x-on:click="dialogOpen = false">Close</button>30- </div>31- </div> 1+ @script 2+ <script> 3+ window.redirectToTarget = () => { 4+ Livewire.navigate(@js(route('target.route', ['param' => $param->id]))) 5+ } 6+ </script> 7+ @endscript 8 9- <div10- x-data="{ dialogOpen: false }"11+ x-on:click="redirectToTarget"12- class="clickable-card"13- >14- <h3>15- Action Title16- <button17- type="button"18- x-on:click.stop.prevent="dialogOpen = true"19- >20- <x-lucide-info />21- </button>22- </h3>23-24- <p>Short description text</p>25-26- <div x-show="dialogOpen" class="dialog-panel">27- <h2>Dialog Title</h2>28- <p>More info goes here.</p>29- <button type="button" x-on:click="dialogOpen = false">Close</button>30- </div>31- </div>Why This Works
- The card itself controls navigation using
redirectToTarget() - The dialog button stops the click from bubbling up (
.stop.prevent) - You decide when navigation should fire not the browser
No hacks. No overlays. Just letting Alpine handle UI, and Livewire handle navigation independently.
Key Lesson
When combining Alpine interactions inside click-based navigation:
- Stop clicks meant for UI elements
- Trigger navigation programmatically
- Keep control where it belongs
Sometimes the cleanest solution starts with simply reading the docs.