Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Livewire Dialog Laravel Package

andisiahaan/livewire-dialog

A Laravel Livewire package for building accessible, customizable modal dialogs with minimal JavaScript. It simplifies opening, closing, and managing dialog state directly from Livewire components.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin wrapper, not a framework. The entire public surface is a Blade component, a state toggle, and event listeners. You can be productive in ten minutes; the risk is not onboarding, it's longevity.

Install

composer require andisiahaan/livewire-dialog

There is no publishable config or migration mentioned in the description, so treat install as a no-op beyond autoloading. Confirm that assumption first:

composer show andisiahaan/livewire-dialog          # version, requires, autoload
ls vendor/andisiahaan/livewire-dialog/resources/views

Check composer.json for the livewire/livewire constraint. The <livewire:dialog> tag syntax is Livewire 3's namespaced-component form; if your app is still on Livewire 2, stop here — there is no fallback syntax documented and you would be the one writing it.

Where to look first, in this order

  1. vendor/andisiahaan/livewire-dialog/resources/views/** — the actual component markup. This tells you the prop names (:open, wire:model, name, maxWidth, whatever they chose), the z-index, and whether content is rendered inline or teleported. The description does not enumerate props; this file does.
  2. src/ — the anonymous or class-based component and any traits. You need the exact method names (open(), close(), toggle()?) and the event names it listens for, because the description only promises "simple methods."
  3. The README on the repo for a Tailwind version assumption (Alpine directives inside usually imply Tailwind classes; check for a @tailwindcss/forms or Alpine dependency).

First use case: the destructive-confirmation dialog

Do this before anything ambitious. It exercises the whole contract — state, slot, dismissal, method call.

// app/Livewire/ManagePosts.php
class ManagePosts extends Component
{
    public bool $confirmingDelete = false;
    public ?int $deletingId = null;

    public function confirm(int $id): void { $this->deletingId = $id; $this->confirmingDelete = true; }
    public function cancel(): void { $this->confirmingDelete = false; $this->deletingId = null; }
    public function delete(): void { /* authorize + delete */ $this->cancel(); }

    public function render() { return view('livewire.manage-posts'); }
}
<livewire:dialog :open="$confirmingDelete">
    <h2>Delete this post?</h2>
    <p>This cannot be undone.</p>
    <button wire:click="cancel">Cancel</button>
    <button wire:click="delete">Delete</button>
</livewire:dialog>

If that renders, animates, closes on Escape, and closing sets $confirmingDelete = false on the server, you have validated the package's core claim. If closing only hides the DOM and leaves your state true, you have found the single most important thing to know before writing forty dialogs — see Gotchas.


Implementation Patterns

1. One boolean per dialog, named by intent

Because visibility is server state, the naming discipline is the architecture:

public bool $showInviteDialog = false;
public bool $showImportDialog = false;

Avoid a single $activeDialog string unless you enjoy @if chains inside slots. Separate booleans keep each dialog's data ($inviteEmail, $importFile) independent and make Livewire::test() assertions trivial.

2. Confirmation pattern (the 80% case)

The highest-value use is gating a destructive or irreversible action behind a second round-trip. The pattern above is the whole thing. Two refinements that matter in production:

  • Re-authorize inside the action, not just at confirm(). The dialog may sit open for minutes.
  • Re-read the target from the database in delete(); don't trust $deletingId alone.

3. Form-in-dialog, with validation errors rendered in place

This is where server-driven dialogs beat JS modals: validation failures land naturally.

<livewire:dialog :open="$showCreateDialog">
    <form wire:submit="save">
        <input type="text" wire:model="title">
        @error('title') <span>{{ $message }}</span> @enderror
        <button type="submit" wire:loading.attr="disabled" wire:target="save">Save</button>
    </form>
</livewire:dialog>

Key rule: the form lives inside the slot, so its markup is part of the parent component's template. Errors, wire:loading, and wire:target all resolve against the parent component — no nested component, no event plumbing, no $parent. This is the package's real selling point over hand-rolled Alpine modals.

Close on success ($this->showCreateDialog = false; at the end of save()), stay open on failure (leave it true). That is the entire UX contract users expect.

4. Long-running action pattern

Pair dialogs with wire:loading.disable on the confirming button and keep the dialog open until the job resolves. If the work is slow, prefer dispatching a queued job and closing immediately with a flash message — a spinner inside a modal for thirty seconds is worse than no modal.

5. Multiple dialogs in one component

Because the package owns the overlay per instance, stacking two open dialogs is a z-index and scroll-lock problem you don't want. Enforce "at most one open" by making the open props mutually exclusive at the method level:

private function closeAll(): void { $this->showInviteDialog = $this->showImportDialog = false; }

6. Testing

Server-driven dialogs are testable without a browser, which is the main TCO argument for choosing this over a JS library:

Livewire::test(ManagePosts::class)
    ->call('confirm', 5)
    ->assertSet('confirmingDelete', true)
    ->assertSee('Delete this post?')
    ->call('delete')
    ->assertSet('confirmingDelete', false);

Note that assertSee passes whether or not the dialog is visually open, depending on whether the package uses @if or CSS classes. Test the state, and separately assert the rendered markup toggles (assertSeeHtml / assertDontSeeHtml) if visibility is @if-gated.

7. Wrap it before you scale it

Given the signals in this package's profile, the cheapest insurance is a one-file indirection. Create resources/views/components/app-dialog.blade.php that renders <livewire:dialog {{ $attributes }}>{{ $slot }}</livewire:dialog>, and use <x-app-dialog> throughout your app. Swapping to Flux, Filament, or a 60-line Alpine component later becomes a single-file change instead of a grep-and-replace across every view.


Gotchas and Tips

Verify that closing syncs state back

"Escape-key and click-outside dismissal" describes client-side behaviour. The critical question is whether dismissal dispatches back to Livewire (e.g. $wire.set('open', false) or $dispatch('dialog-closed')) or merely hides the element. If it only hides, your $showDialog stays true, and the next render reopens the dialog — a bug that reproduces intermittently and blames the framework. Test Escape explicitly with Dusk or a quick manual pass on day one. If it doesn't sync, add x-on:close.window="$wire.set('showX', false)" or a close() method on the package component.

Slot content is in the parent's DOM

Unless the package uses x-teleport, the dialog markup renders where you put it. Inside a <table> row, inside a position: relative container with overflow: hidden, or inside an element with a transform, your "fixed" overlay will be clipped or mispositioned. If the package doesn't teleport, wrap the component in x-teleport="body" yourself (Alpine ships with Livewire).

Focus trap and aria-modal are claims, not guarantees

The description advertises "accessibility details." Zero-star packages rarely ship a complete focus trap (first-focus, tab cycling, focus restoration on close, inert on the background). Audit it: open the dialog, press Tab repeatedly, and check whether focus escapes to the page behind. If it does, add x-trap from @focus-trap/alpine or accept the gap knowingly. Do not put a compliance claim in a ticket based on the README.

Body scroll lock leaks

A common failure mode is document.body.style.overflow = 'hidden' set on open and never cleared when the component is removed by a Livewire morph or wire:navigate. Symptom: the page inexplicably stops scrolling after navigation. Check the Alpine x-init/destroy pairing before shipping.

Click-outside during text selection

Dragging to select text inside the dialog and releasing outside fires the click-outside handler. If it's wired to mousedown rather than click paired with a guard, users will lose their typed content. Reproduce this deliberately; if broken, disable outside-dismiss (:close-on-click-outside="false" if available) rather than fight it.

Escape collides with everything else

If you already have a command palette, a select dropdown, or an Alpine x-on:keydown.escape handler, the dialog's Escape listener will race it. Scope it (x-on:keydown.escape.window with a "topmost dialog" guard) or you'll get both closing at once.

Styling is probably Tailwind-plus-Alpine, and probably not published

If the package ships its views inline, you cannot restyle markup without publishing or overriding a vendor view. Check for a --tag= publish option. If there is none, your only lever is CSS overrides on the classes it emits — brittle, and the reason the wrapper component in Pattern 7 is worth the ten minutes.

wire:key matters on repeated dialogs

Rendering a dialog per row in a @foreach (:open="$confirmingId === $post->id") works, but without a unique wire:key Livewire's morph can reuse a dialog's DOM and leave stale slot content from a different row. Prefer one dialog per component with a $targetId property — cleaner and immune to this class of bug.

Validation state leaks between opens

$this->resetValidation() on open is usually necessary; otherwise the red text from the last attempt greets the user before they type. Same for $this->reset('form') if the dialog should open blank.

The maintenance risk is the actual risk

Zero stars, zero dependents, and a single publish date tell you the bus factor is one and the integration surface is undocumented beyond a paragraph. Concretely:

  • Pin the exact version ("andisiahaan/livewire-dialog": "1.0.*") and commit the lock file.
  • Do not let it into a package you redistribute without reading the MIT obligations (trivial here, but confirm the license file exists in vendor/).
  • Expect no Livewire 4 support until proven otherwise; the wrapper component means a future migration is a file, not a quarter.
  • Keep the fallback written down: Flux UI's <flux:modal>, Filament's modal for panel apps, or ~40 lines of Alpine + x-trap. All three are drop-in replacements behind the wrapper.

When to skip it entirely

If you need anything beyond open/close — draggable, resizable, stacked sheets, nested confirms, or slide-over variants — this package will cost you more in workarounds than the Alpine component it wraps would cost to write. Its honest scope is the simple confirmation and the form-in-a-box, and it does those well precisely because it does nothing else.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route