├── src ├── Commands │ └── .gitkeep ├── Livewire │ └── Modal.php └── LivewireModalServiceProvider.php ├── config └── livewire-modal.php ├── CHANGELOG.md ├── pint.json ├── resources └── views │ ├── components │ ├── slideover.blade.php │ ├── modal.blade.php │ └── stack.blade.php │ ├── directive.blade.php │ └── livewire │ └── modal.blade.php ├── LICENSE.md ├── composer.json └── README.md /src/Commands/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/livewire-modal.php: -------------------------------------------------------------------------------- 1 | 'right', 3 | ]) 4 | 5 | 6 | {{ $slot }} 7 | 8 | -------------------------------------------------------------------------------- /resources/views/components/modal.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'position' => 'center', 3 | 'xData' => '', 4 | ]) 5 | 6 |
class(['max-w-full min-w-0 transition']) !!}> 22 | {{ $slot }} 23 |
24 | -------------------------------------------------------------------------------- /src/Livewire/Modal.php: -------------------------------------------------------------------------------- 1 | , params: array }> 19 | */ 20 | public array $components = []; 21 | 22 | public function mount(null|string|bool $stack = null): void 23 | { 24 | if (is_bool($stack)) { 25 | $this->stack = $stack ? 'default' : null; 26 | } else { 27 | $this->stack = $stack; 28 | } 29 | } 30 | 31 | public function render(): \Illuminate\Contracts\View\View 32 | { 33 | return view('livewire-modal::livewire.modal'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LivewireModalServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('livewire-modal') 24 | ->hasViews(); 25 | } 26 | 27 | public function bootingPackage(): void 28 | { 29 | $this->callAfterResolving('livewire', function (LivewireManager $livewire, Application $app) { 30 | $livewire->component('modal', Modal::class); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) elegantly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/views/components/stack.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'direction' => null, 3 | 'fullscreen' => false, 4 | ]) 5 | 6 |
class([ 7 | 'pt-20 sm:p-5' => !$fullscreen, 8 | 'size-full min-w-0 flex pointer-events-none [&>*]:pointer-events-auto', 9 | ]) !!} x-data="{ 10 | modalDirection: {{ Js::from($direction) }}, 11 | modalPosition: [null, null], 12 | computeModalDirection() { 13 | const [px, py] = this.modalPosition; 14 | 15 | const directions = { 16 | left: [1, 0], 17 | right: [-1, 0], 18 | top: [0, 1], 19 | bottom: [0, -1], 20 | }; 21 | 22 | return directions[px || py] || [0, -1]; 23 | }, 24 | computeModalPosition() { 25 | const parent = this.$el.getBoundingClientRect(); 26 | const rect = this.$el.firstElementChild.getBoundingClientRect(); 27 | 28 | const dx = parent.width - rect.right; 29 | const dy = parent.height - rect.bottom; 30 | 31 | const px = dx === rect.left ? 'center' : dx < rect.left ? 'right' : 'left'; 32 | const py = dy === rect.top ? 'center' : dy < rect.top ? 'bottom' : 'top'; 33 | 34 | return [px, py]; 35 | }, 36 | init() { 37 | if (!this.modalDirection) { 38 | this.$nextTick(() => { 39 | this.modalPosition = this.computeModalPosition(); 40 | this.modalDirection = this.computeModalDirection(); 41 | }); 42 | } 43 | 44 | }, 45 | modalAttributes: { 46 | ['x-show']() { return isModalStacked ? modalHistory.includes(modalId) : isModalActive; }, 47 | ['x-bind:inert']() { return !isModalActive }, 48 | ['x-bind:style']() { 49 | if (isModalStacked) { 50 | const [dx, dy] = (this.modalDirection ?? [0, 0]); 51 | 52 | return { 53 | '--dx': dx, 54 | '--dy': dy, 55 | '--i': modalIndexReversed, 56 | transform: `scale(calc(1 - 0.05 * var(--i))) translate(calc(2rem * var(--dx) * var(--i)), calc(2rem * var(--dy) * var(--i)))`, 57 | opacity: modalIndexReversed <= 2 ? 1 : 0, 58 | }; 59 | } 60 | 61 | return {}; 62 | }, 63 | }, 64 | }"> 65 | {{ $slot }} 66 |
67 | -------------------------------------------------------------------------------- /resources/views/directive.blade.php: -------------------------------------------------------------------------------- 1 | 83 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elegantly/livewire-modal", 3 | "description": "Modal for your Livewire App. Done right.", 4 | "keywords": [ 5 | "elegantly", 6 | "laravel", 7 | "livewire-modal" 8 | ], 9 | "homepage": "https://github.com/ElegantEngineeringTech/livewire-modal", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Quentin Gabriele", 14 | "email": "quentin.gabriele@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "illuminate/contracts": "^10.0||^11.0||^12.0", 21 | "livewire/livewire": "^3.0", 22 | "spatie/laravel-package-tools": "^1.16" 23 | }, 24 | "require-dev": { 25 | "laravel/pint": "^1.14", 26 | "nunomaduro/collision": "^8.1.1||^7.10.0", 27 | "larastan/larastan": "^2.9||^3.0", 28 | "orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0", 29 | "pestphp/pest": "^3.0", 30 | "pestphp/pest-plugin-arch": "^3.0", 31 | "pestphp/pest-plugin-laravel": "^3.0", 32 | "phpstan/extension-installer": "^1.3||^2.0", 33 | "phpstan/phpstan-deprecation-rules": "^1.1||^2.0", 34 | "phpstan/phpstan-phpunit": "^1.3||^2.0" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Elegantly\\LivewireModal\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Elegantly\\LivewireModal\\Tests\\": "tests/", 44 | "Workbench\\App\\": "workbench/app/", 45 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 46 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 47 | } 48 | }, 49 | "scripts": { 50 | "post-autoload-dump": [ 51 | "@clear", 52 | "@prepare", 53 | "@composer run prepare" 54 | ], 55 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 56 | "analyse": "vendor/bin/phpstan analyse", 57 | "test": "vendor/bin/pest", 58 | "test-coverage": "vendor/bin/pest --coverage", 59 | "format": "vendor/bin/pint", 60 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 61 | "build": "@php vendor/bin/testbench workbench:build --ansi", 62 | "serve": [ 63 | "Composer\\Config::disableProcessTimeout", 64 | "@build", 65 | "@php vendor/bin/testbench serve --ansi" 66 | ], 67 | "lint": [ 68 | "@php vendor/bin/pint --ansi", 69 | "@php vendor/bin/phpstan analyse --verbose --ansi" 70 | ] 71 | }, 72 | "config": { 73 | "sort-packages": true, 74 | "allow-plugins": { 75 | "pestphp/pest-plugin": true, 76 | "phpstan/extension-installer": true 77 | } 78 | }, 79 | "extra": { 80 | "laravel": { 81 | "providers": [ 82 | "Elegantly\\LivewireModal\\LivewireModalServiceProvider" 83 | ], 84 | "aliases": { 85 | "LivewireModal": "Elegantly\\LivewireModal\\Facades\\LivewireModal" 86 | } 87 | } 88 | }, 89 | "minimum-stability": "dev", 90 | "prefer-stable": true 91 | } 92 | -------------------------------------------------------------------------------- /resources/views/livewire/modal.blade.php: -------------------------------------------------------------------------------- 1 |
154 | 155 | @foreach ($components as ['id' => $id, 'component' => $component, 'props' => $props]) 156 |
167 | @livewire($component, $props, key("{$this->getId()}.modalComponents.{$id}.component")) 168 |
169 | @endforeach 170 | 171 | @include('livewire-modal::directive') 172 |
173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livewire Modals. Done Right. 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/elegantly/livewire-modal.svg?style=flat-square)](https://packagist.org/packages/elegantly/livewire-modal) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/ElegantEngineeringTech/livewire-modal/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/ElegantEngineeringTech/livewire-modal/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/ElegantEngineeringTech/livewire-modal/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/ElegantEngineeringTech/livewire-modal/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/elegantly/livewire-modal.svg?style=flat-square)](https://packagist.org/packages/elegantly/livewire-modal) 7 | 8 | This package allows you to seamlessly open Livewire components inside modals or slideovers with powerful features: 9 | 10 | - Support for modals, slideovers, and similar UI patterns. 11 | - Nested and stacked modals. 12 | - Custom styling and animations, with optional presets. 13 | - Preloading components for faster interactions. 14 | 15 | ## Requirements 16 | 17 | - `livewire/livewire`: v3 18 | - `tailwindcss`: v3 (not yet tested with v4) 19 | 20 | ## How It Works 21 | 22 | This package provides a single Livewire `Modal` component that should be placed at the end of your `body` tag. This component dynamically renders and manages all modal instances while maintaining a modal history. 23 | 24 | Modals can be opened and closed by dispatching `modal-open` and `modal-close` events. 25 | 26 | Any Livewire component can be used as a modal without requiring special interfaces or base components—just use your existing components as they are. 27 | 28 | ## Installation 29 | 30 | Install the package via Composer: 31 | 32 | ```bash 33 | composer require elegantly/livewire-modal 34 | ``` 35 | 36 | To customize modal behavior, publish the views with: 37 | 38 | ```bash 39 | php artisan vendor:publish --tag="livewire-modal-views" 40 | ``` 41 | 42 | ## Usage 43 | 44 | ### Configuring Tailwind CSS 45 | 46 | Since the modal component is styled using Tailwind CSS, you must include its views in your Tailwind configuration file: 47 | 48 | ```js 49 | export default { 50 | content: [ 51 | "./vendor/elegantly/livewire-modal/resources/views/**/*.blade.php", 52 | ], 53 | }; 54 | ``` 55 | 56 | ### Setting Up Your Application 57 | 58 | Add the modal manager component `` at the end of your `body` tag, typically in your layout views: 59 | 60 | ```html 61 | 62 | ... 63 | 64 | 65 | ``` 66 | 67 | ### Preparing Your Modals 68 | 69 | Any Livewire component can be displayed as a modal. However, certain features, such as stacking, require additional customization. 70 | 71 | #### Creating a Simple Modal Component 72 | 73 | This package provides two Blade components to simplify stacking and positioning: 74 | 75 | - `x-livewire-modal::stack`: Provides a basic layout with stacking capabilities. 76 | - `x-livewire-modal::modal`: Handles positioning and stacking. 77 | 78 | Wrap your content within these components: 79 | 80 | ```html 81 | 82 | 86 |
87 |

88 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam 89 | rhoncus, augue eget vulputate vehicula, justo dui auctor est, at 90 | iaculis urna orci ut nunc. 91 |

92 |
93 |
94 |
95 | ``` 96 | 97 | #### Controlling the Modal Position 98 | 99 | By default, modals are centered, but their position can be adjusted using the `position` prop: 100 | 101 | ```html 102 | 103 | ... 104 | 105 | ``` 106 | 107 | ```html 108 | 109 | ... 110 | 111 | ``` 112 | 113 | #### Fullscreen Modal 114 | 115 | To make a modal fullscreen, use the `fullscreen` prop: 116 | 117 | ```html 118 | ... 119 | ``` 120 | 121 | #### Creating a Slideover Component 122 | 123 | ```html 124 | 125 | 128 |
129 |

130 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam 131 | rhoncus, augue eget vulputate vehicula, justo dui auctor est, at 132 | iaculis urna orci ut nunc. 133 |

134 |
135 |
136 |
137 | ``` 138 | 139 | ### Opening a Modal 140 | 141 | To open a modal, dispatch a `modal-open` event: 142 | 143 | ```html 144 | 147 | ``` 148 | 149 | ```html 150 | 155 | ``` 156 | 157 | ```js 158 | Livewire.dispatch("modal-open", { 159 | component: "users.show", 160 | props: { userId: 1 }, 161 | }); 162 | ``` 163 | 164 | ### Preloading a Modal 165 | 166 | To preload a modal, dispatch a `modal-preload` event with the same props used to open it: 167 | 168 | ```js 169 | Livewire.dispatch("modal-preload", { 170 | component: "users.show", 171 | props: { userId: 1 }, 172 | }); 173 | ``` 174 | 175 | ### Preloading a Modal on Hover 176 | 177 | Using the custom Alpine directive, you can preload a modal when the user starts hovering over a button. This improves UX by ensuring faster modal openings. 178 | 179 | ```html 180 | 185 | ``` 186 | 187 | ### Closing the Current Modal 188 | 189 | To close the currently active modal, dispatch a `modal-close` event: 190 | 191 | ```html 192 | 193 | ``` 194 | 195 | ```html 196 | 197 | ``` 198 | 199 | ```js 200 | Livewire.dispatch("modal-close"); 201 | ``` 202 | 203 | ### Closing All Modals 204 | 205 | To close all modals at once: 206 | 207 | ```html 208 | 209 | ``` 210 | 211 | ```html 212 | 213 | ``` 214 | 215 | ```js 216 | Livewire.dispatch("modal-close-all"); 217 | ``` 218 | 219 | ## Testing 220 | 221 | Run the test suite with: 222 | 223 | ```bash 224 | composer test 225 | ``` 226 | 227 | ## Changelog 228 | 229 | For recent changes, see [CHANGELOG](CHANGELOG.md). 230 | 231 | ## Contributing 232 | 233 | See [CONTRIBUTING](CONTRIBUTING.md) for contribution guidelines. 234 | 235 | ## Security Vulnerabilities 236 | 237 | For information on reporting security vulnerabilities, please review [our security policy](../../security/policy). 238 | 239 | ## Credits 240 | 241 | - [Quentin Gabriele](https://github.com/QuentinGab) 242 | - [All Contributors](../../contributors) 243 | 244 | ## License 245 | 246 | This package is licensed under the MIT License. See [License File](LICENSE.md) for details. 247 | --------------------------------------------------------------------------------