corepine/modal
Corepine Modal
Corepine Modal is a stack-based modal system for Laravel with two runtime modes:
-
standalone Alpine + Blade modals
-
Livewire stack-based modals
-
modal(dialog) -
drawer(left or right panel) -
sheet(bottom sheet)
It supports:
- modal stacks (open child modals on top of parent modals)
- declarative shell actions
- strongly typed modal classes (
extends Corepine\Modal\Modal) - configurable event names for package-safe integrations
- standalone Blade-only modals (no Livewire modal class required)
Requirements
- PHP
^8.2|^8.3|^8.4 - Laravel
^11.0|^12.0|^13.0 - Livewire
^3.7|^4.0
Livewire is required by the package because stack mode uses a Livewire host. Standalone Alpine + Blade usage is still fully supported.
Installation
composer require corepine/modal
Publish config:
php artisan vendor:publish --tag=corepine-modal-config
Setup
Livewire Stack Mode
Render the host once in your layout:
<x-corepine.modal.assets />
Standalone Alpine + Blade Mode
You can use <x-corepine.modal /> directly with browser events and no Livewire modal class.
The host is not required for standalone-only usage.
Tailwind Setup
Add the package stylesheet to your main CSS entry:
@import "../../vendor/corepine/modal/resources/css/app.css";
The package CSS already includes Tailwind @source paths for its own views, PHP classes, and config.
If you publish corepine-modal.php and add custom modal size classes there, include the published config in your app CSS source list so Tailwind can keep those classes:
@source "../../config/corepine-modal.php";
Quick Start (Livewire Stack Mode)
<?php
namespace App\Livewire\Modals;
use App\Models\User;
use Corepine\Modal\Actions\Action;
use Corepine\Modal\Enums\ModalType;
use Corepine\Modal\Modal;
use Corepine\Support\Enums\Placement;
class EditUser extends Modal
{
public User $user;
public static function modalAttributes(): array
{
return [
'type' => ModalType::Modal,
'placement' => Placement::Center,
'origin' => Placement::Center,
'shell' => true,
'heading' => 'Edit User',
'description' => 'Update account details',
'showClose' => true,
'dismissible' => true,
'closeOnEscape' => true,
'actions' => [
Action::make('cancel')->label('Cancel')->close(),
Action::make('save')->label('Save')->primary()->action('save'),
],
];
}
}
Open it from Blade:
<button
type="button"
onclick="Livewire.dispatch('modal.open', { component: 'modals.edit-user', arguments: { user: {{ $user->id }} } })"
>
Edit User
</button>
Open / Close APIs
From a modal class:
$this->openModal('modals.edit-user', ['user' => 5]);
$this->openBottomSheet('modals.user-sheet', ['user' => 5]);
$this->closeModal();
$this->closeModal(
destroy: false,
dispatch: ['users-refreshed' => ['user' => 5]],
dispatchTo: ['orders.table' => ['sync-user' => ['user' => 5]]],
);
$this->closeTopModal(
layers: 2,
dispatch: ['users-refreshed' => ['user' => 5]],
);
$this->closeAll(
dispatchTo: ['orders.table' => ['sync-user' => ['user' => 5]]],
);
From Blade helpers:
<x-corepine.modal.actions.open component="modals.edit-user" :arguments="['user' => $user->id]">
<button type="button">Edit</button>
</x-corepine.modal.actions.open>
<x-corepine.modal.actions.open modal-id="user-sheet">
<button type="button">Open Sheet</button>
</x-corepine.modal.actions.open>
<x-corepine.modal.actions.close
layers="1"
:destroy="true"
:dispatch="['users-refreshed' => ['user' => $user->id]]"
:dispatch-to="['orders.table' => ['sync-user' => ['user' => $user->id]]]"
>
Close
</x-corepine.modal.actions.close>
dispatch fires regular Livewire/browser events after the close finishes.
dispatchTo fires Livewire targeted events after the close finishes.
Use modal-id on open/close helpers when you want to target a standalone <x-corepine.modal id="..." /> by id.
Quick Start (Standalone Alpine + Blade Mode)
Use this when you do not need a Livewire modal class:
<button
type="button"
onclick="window.dispatchEvent(new CustomEvent('modal.open', { detail: { id: 'user-sheet' } }))"
>
Open Sheet
</button>
<x-corepine.modal id="user-sheet" type="sheet" heading="User Details">
<p class="text-sm text-zinc-600">Standalone modal body</p>
<x-slot:footer>
<button
type="button"
class="rounded-md border px-3 py-2 text-sm"
onclick="window.dispatchEvent(new CustomEvent('modal.close', { detail: { id: 'user-sheet' } }))"
>
Close
</button>
</x-slot:footer>
</x-corepine.modal>
Standalone named slots:
header: custom header content. Slot attributes are merged onto the header wrapper classes.- When
headerslot is provided (even empty), it overrides built-in heading/description/close rendering. footer: custom footer content.
Example:
<x-corepine.modal id="custom-header-modal" show-close="false">
<x-slot:header class="font-bold text-lg" data-testid="custom-header">
Custom Header
</x-slot:header>
<p>Body</p>
</x-corepine.modal>
Standalone browser events:
modal.openmodal.closemodal.toggle
Each event accepts { id?: string } in detail.
Modal Attributes
The canonical shell/action API uses actions (not legacy keys).
| Key | Type | Default | Notes |
|---|---|---|---|
type |
modal | drawer | sheet |
modal |
Presentation type. |
placement |
Placement | string |
by type | modal: center/top/bottom/left/right, drawer: left/right, sheet: forced bottom. |
origin |
Placement | string |
follows type/placement | Transform origin; same value set as placement vocabulary. |
size |
string |
default |
Width token from config sizes, or custom class string. |
height |
string | number | null |
null |
Panel height (modal/drawer) and initial sheet height. |
maxHeight |
string | number | null |
null |
Shared max-height cap for all types. |
dismissible |
bool |
true |
Scrim click closes when true. |
draggable |
bool |
type-aware | Sheet drag/resize behavior. |
showDragHandle |
bool |
type-aware | Sheet handle visibility. |
dragCloseThreshold |
float |
0.5 |
Sheet drag-close ratio. |
closeOnEscape |
bool |
true |
Escape closes top layer. |
closeAllOnEscape |
bool |
false |
Escape closes full stack. |
destroyOnClose |
bool |
true |
Remove closed layers from host state. |
dispatch |
array |
[] |
Default events to dispatch after close. |
dispatchTo |
array |
[] |
Default targeted Livewire events to dispatch after close. |
dispatchCloseEvent |
bool |
false |
Emits the built-in modal.component-closed notification for that layer. |
blur |
bool |
false |
Scrim blur effect. |
shell |
bool |
true |
Enables built-in shell header/body/footer structure. |
heading |
string | null |
null |
Shell heading text. |
description |
string | null |
null |
Shell description text. |
showClose |
bool | null |
auto |
Built-in shell close icon. Defaults to visible only when built-in heading or description is present. |
footerActionsAlignment |
Alignment | string |
end |
start, center, end. |
actions |
array |
[] |
Declarative shell actions (close / method). |
class |
string |
'' |
Extra panel classes. |
Type Behavior Rules
sheet: always renders from bottom and always usesplacement=bottom,origin=bottom.drawer: onlyleftandrightare valid.modal: supports all five placement values and now fully respects bothplacementandorigin.
Declarative Actions
Action payloads can be raw arrays or fluent Action::make(...) objects.
use Corepine\Modal\Actions\Action;
'actions' => [
Action::make('cancel')
->label('Cancel')
->close(),
Action::make('save')
->label('Save')
->primary()
->action('save'),
]
Supported fluent helpers include:
method()/action()close(layers, destroy, closeAll)dispatch()/dispatchTo()on close actionsdisabled()visible()color()and shortcuts (primary,danger,success,warning,info,gray,dark)accent()outline()attributes()
Event System
Default incoming events:
modal.openmodal.open-sheetmodal.closemodal.close-topmodal.close-allmodal.destroymodal.resetmodal.toggle
Default outgoing events:
modal.openedmodal.closedmodal.changedmodal.all-closedmodal.component-closed
Event Customization
You can rename both incoming and outgoing events in config/corepine-modal.php:
'events' => [
'listen' => [
'open' => 'acme.modal.open',
'close' => 'acme.modal.close',
],
'dispatch' => [
'opened' => 'acme.modal.opened',
],
],
For package integrations, do not hardcode event strings. Resolve them from the service:
use Corepine\Modal\Facades\Modal;
$openEvent = Modal::event()->openModal();
$closeEvent = Modal::event()->closeModal();
Configuration
Main sections in config/corepine-modal.php:
events.listen: incoming event namesevents.dispatch: outgoing event namesdefaults.attributes: global modal attribute defaultssizes: modal width tokens
Example Size Override
'sizes' => [
'default' => 'max-w-xl sm:max-w-full',
'editor' => 'max-w-[960px]',
],
Blade Components (Canonical)
<x-corepine.modal.assets /><x-corepine.modal /><x-corepine.modal.layout /><x-corepine.modal.footer /><x-corepine.modal.actions.open /><x-corepine.modal.actions.close />