Table of Contents & Menu
Navigation

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>