Using Hyvä Modals Without a PHP ViewModel
Hyvä modals can be used without a PHP ViewModel. This requires manually adding an overlay element with x-spread="overlay()" x-bind="overlay()" to your markup.
Why x-spread and x-bind?
Use both x-spread and x-bind for compatibility with Alpine.js v2 and v3. If targeting a single version, use:
- Alpine.js v2:
x-spread="overlay()" - Alpine.js v3:
x-bind="overlay()"
The dialog content must also be wrapped in an element with x-ref="dialog".
This approach is useful for scenarios like embedding modals within CMS HTML content.
Example: Basic Standalone Modal
Here's an example of a modal with a counter, implemented without the PHP Modal ViewModel:
<div x-data="{...hyva.modal(), n: 0}">
<button @click="show" type="button" class="btn mt-40" aria-haspopup="dialog">
<?= $escaper->escapeHtml(__('Open')) ?>
</button>
<div x-cloak x-spread="overlay()" x-bind="overlay()"
class="fixed inset-0 flex items-center justify-center text-left bg-black bg-opacity-50 z-30">
<div x-ref="dialog" role="dialog" aria-labelledby="the-label"
class="inline-block max-h-screen overflow-auto bg-white shadow-xl rounded-lg p-10 text-gray-700">
<div id="the-label"><?= $escaper->escapeHtml(__('Modal without PHP')) ?></div>
<div>
<?= $escaper->escapeHtml(__('Counter:'))?> <span x-text="n">?</span>
<button class="btn" @click="n++"><?= $escaper->escapeHtml('Increment') ?></button>
</div>
<div class="mt-20 flex justify-between gap-2">
<button @click="hide" type="button" class="btn">
<?= $escaper->escapeHtml(__('Cancel')) ?>
</button>
<button x-focus-first @click="alert('click')" type="button" class="btn btn-primary">
<?= $escaper->escapeHtml(__('Okay')) ?>
</button>
</div>
</div>
</div>
</div>
Nested Standalone Modals
For nested standalone modals, you must manually manage dialog reference names. Specify these names in your show and overlay calls:
<div x-data="hyva.modal()">
<button @click="show('outer', $event)" type="button" class="btn mt-40" aria-haspopup="dialog"><?= $escaper->escapeHtml(__('Open Outer')) ?></button>
<div x-cloak x-bind="overlay('outer')" x-spread="overlay('outer')"
class="fixed inset-0 flex items-center justify-center text-left bg-black bg-opacity-50 z-30">
<div x-ref="outer" role="dialog" aria-labelledby="outer-label"
class="inline-block max-h-screen overflow-auto bg-white shadow-xl rounded-lg p-10 text-gray-700">
<div id="outer-label"><?= $escaper->escapeHtml(__('Outer Modal')) ?></div>
<div>
<div x-cloak x-bind="overlay('inner')" x-spread="overlay('inner')"
class="fixed inset-0 flex items-center justify-center text-left bg-black bg-opacity-50">
<div x-ref="inner" role="dialog" aria-labelledby="inner-label"
class="inline-block max-h-screen overflow-auto bg-white shadow-xl rounded-lg p-10 text-gray-700">
<div id="inner-label"><?= $escaper->escapeHtml(__('Inner Modal')) ?></div>
<div class="mt-20 flex justify-between gap-2">
<button @click="hide" type="button" class="btn">
<?= $escaper->escapeHtml(__('Cancel')) ?>
</button>
<button x-focus-first @click="alert('It is done.')" type="button" class="btn btn-primary">
<?= $escaper->escapeHtml(__('Do it!')) ?>
</button>
</div>
</div>
</div>
</div>
<div class="mt-20 flex justify-between gap-2">
<button @click="hide" type="button" class="btn">
<?= $escaper->escapeHtml(__('Cancel')) ?>
</button>
<button x-focus-first @click="show('inner', $event)" type="button" class="btn btn-primary" aria-haspopup="dialog">
<?= $escaper->escapeHtml(__('Open Inner')) ?>
</button>
</div>
</div>
</div>
</div>