corepine/modal

Reusable Alpine + Livewire modal stack for Corepine packages.

Maintainers

Package info

github.com/corepine/modal

pkg:composer/corepine/modal

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-03-14 17:42 UTC

This package is not auto-updated.

Last update: 2026-03-16 08:45:21 UTC


README

Corepine Modal is a stack-based modal package for Livewire v3/v4 with Laravel 11/12/13 support.

It is built around:

  • dispatch events (no $emit)
  • reusable modal classes (extends Modal)
  • child modal stacking
  • safe model argument resolution from IDs

Requirements

  • PHP ^8.2|^8.3|^8.4
  • Laravel ^11.0|^12.0|^13.0
  • Livewire ^3.7|^4.0

Installation

composer require corepine/modal

Add The Host

Add once in your main layout:

<x-corepine-modal />

or:

@corepineModal

Tailwind v4 Setup

Import package CSS in your app app.css:

@import "../../vendor/corepine/modal/resources/css/app.css";

No tailwind.config.js is required for this package setup.

Create A Modal

Use Corepine\Modal\Modal as the base class:

<?php

namespace App\Livewire\Modals;

use App\Models\User;
use Corepine\Modal\Modal;

class EditUser extends Modal
{
    public User $user;

    public static function modalSize(): string
    {
        return 'xl'; // token from config('corepine-modal.sizes')
    }

    public static function modalAttributes(): array
    {
        return [
            'closeOnEscape' => true,
            'closeOnClickAway' => true,
            'blur' => true,
            'closeOnEscapeIsForceful' => false,
            'destroyOnClose' => true,
            'dispatchCloseEvent' => true,
            'class' => 'p-6',
        ];
    }

    public function save(): void
    {
        // ...
        $this->closeModal();
    }

    public function render()
    {
        return view('livewire.modals.edit-user');
    }
}

If you prefer, you can set size directly in modalAttributes():

public static function modalAttributes(): array
{
    return [
        'size' => '3xl',
        // or raw classes:
        // 'size' => 'max-w-[960px] sm:max-w-full',
        'blur' => true,
        'class' => 'p-6',
    ];
}

Open Modals (dispatch-first)

Outside Livewire (JS)

<button onclick="Livewire.dispatch('openModal', { component: 'modals.edit-user', arguments: { user: 5 } })">
    Edit
</button>

Inside Livewire Blade

<button wire:click="$dispatch('openModal', { component: 'modals.edit-user', arguments: { user: {{ $user->id }} } })">
    Edit
</button>

Open By Class Path

<button wire:click="$dispatch('openModal', { component: '{{ \App\Livewire\Modals\EditUser::class }}', arguments: { user: {{ $user->id }} } })">
    Edit
</button>

Blade Helper Component

<x-corepine-open-modal
    :component-class="\App\Livewire\Modals\EditUser::class"
    :arguments="['user' => $user->id]"
    class="p-8 bg-white border border-zinc-200 rounded-3xl"
    size="2xl"
    blur="true"
>
    <button type="button">Edit</button>
</x-corepine-open-modal>

You can also pass raw classes instead of a size token:

<x-corepine-open-modal
    component="modals.edit-user"
    :arguments="['user' => $user->id]"
    size="max-w-[900px] sm:max-w-full"
/>

class is the styling hook for modal surface styling (background, border, rounded, padding, etc.). When used on <x-corepine-open-modal ...>, class is forwarded to the modal component (not the trigger wrapper).

Closing Modals

From Blade:

<button wire:click="$dispatch('closeModal')">Close</button>
<button wire:click="$dispatch('closeTopModal', { count: 2 })">Close 2</button>
<button wire:click="$dispatch('closeAllModals')">Close All</button>

From modal class:

// Close current modal
$this->closeModal();

// Close current + previous modal
$this->skipPreviousModal()->closeModal();

// Close current + N previous
$this->skipPreviousModals(2)->closeModal();

// Force close everything
$this->forceClose()->closeModal();

// Close and dispatch to other components
$this->closeModalWithEvents([
    \App\Livewire\Users\Table::class => ['usersRefreshed', [$this->user->id]],
]);

Safe Model Argument Resolution

If your modal has typed public properties (for example public User $user), passing user: 5 will resolve the model via route binding before the component mounts.

Enums are also resolved via tryFrom when type-hinted.

Prevent Close On Escape / Click Away

The active modal receives these events:

  • closingModalOnEscape
  • closingModalOnClickAway

Inside modal Blade you can cancel closing:

@script
<script>
    $wire.on('closingModalOnEscape', (payload) => {
        if ($wire.isDirty) payload.closing = false;
    });
</script>
@endscript

Config

Publish config:

php artisan vendor:publish --tag=corepine-modal-config

Config file: config/corepine-modal.php

You can customize:

  • host component name
  • incoming/outgoing event names
  • modal defaults
  • size tokens
  • default blur

Example:

'sizes' => [
    'default' => 'max-w-lg sm:max-w-full',
    'sheet' => 'max-w-[92vw]',
    'dialog' => 'max-w-2xl',
    'editor' => 'max-w-5xl',
],

How this works:

  • left side (default, sheet, dialog, editor) is the size name/token
  • right side is the Tailwind width classes applied to the modal component

So:

  • size="dialog" applies max-w-2xl
  • size="editor" applies max-w-5xl
  • if no size is provided, default is used

Use token from Blade:

<x-corepine-open-modal size="editor" ... />

Use token from modal class:

public static function modalSize(): string
{
    return 'editor';
}

You can also bypass the token map and pass raw classes directly:

<x-corepine-open-modal size="max-w-[900px] sm:max-w-full" ... />

Config Service

Use Corepine\Modal\Support\ModalConfig if you need consistent package values in your own classes:

$modalConfig = app(\Corepine\Modal\Support\ModalConfig::class);
$openEvent = $modalConfig->listenEvent('open'); // openModal
$closedEvent = $modalConfig->dispatchEvent('closed'); // modalClosed

Testing

composer test

Package tests are written with Pest.