Skip to content

Loading modal contents with JavaScript

Sometimes it is required to open a modal and show contents loaded with fetch or Ajax.
There are many ways to achieve this, and there can be many variations to this requirement.
As the saying goes: the devil is in the details.
For example, should a loading animation be shown or not? And if so, when?

  1. The visitor clicks > Open the modal > Show a loading animation > Show content when loaded
  2. The visitor clicks > Show a loading animation > Open the modal with content when loaded

Tutorial Scenario

This tutorial implements one example that you can then hopefully adjust as needed.
We will render a product slider as the modal contents.
Again, adjust as needed. This example is meant to get you started.

1. Create a Magento Module

Since our functionality is not limited to frontend side code that can live inside a theme, we need a Magento module.
We will use the example module name Hyva_Example.

The steps to create a Magento module are out of scope of this tutorial.

2. Create endpoint to serve the modal content

We need some kind of API to serve the desired content. In this example, we will use a standard Magento controller action.

Custom API or standard?

Depending on your needs, you might be able to get away with an exiting API, like the Magento GraphQL or REST API.
If so, you don't need to build the endpoint, but you will need to adjust the JavaScript to load the content accordingly.

We need three things in our module:

A frontend route definition

Create the file etc/frontend/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="hyva_example" frontName="example">
            <module name="Hyva_Example" />

The action class

Create the file Controller/Modal/Content.php


namespace Hyva\Example\Controller\Modal;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\PageFactory;

class Content implements HttpGetActionInterface
    private PageFactory $pageFactory;

    public function __construct(PageFactory $pageFactory)
        $this->pageFactory = $pageFactory;

    public function execute()
        return $this->pageFactory->create();

The layout XML file

Create the file view/frontend/layout/hyva_example_modal_content.xml

<?xml version="1.0"?>
<page xmlns:xsi="" layout="1column"
    <update handle="hyva_product_slider" />
        <referenceContainer name="content">
            <!-- Important! The wrapper ID is specified in the htmlId attribute: -->
            <container name="slider-wrapper" htmlTag="div" htmlId="dom-element-id">
                <block name="slider" template="Magento_Catalog::product/slider/product-slider.phtml">
                        <argument name="class" xsi:type="string" translate="true">class-slider</argument>
                        <argument name="category_ids" xsi:type="string">22</argument>
                        <argument name="include_child_category_products" xsi:type="boolean">true</argument>
                        <argument name="page_size" xsi:type="string">100</argument>
                        <argument name="hide_rating_summary" xsi:type="boolean">true</argument>
                        <argument name="hide_details" xsi:type="boolean">true</argument>
Adjust the slider arguments to match your requirements.

Remember to enable the new module, and flush the Magento cache after making any changes.

3. Create the modal

The high level approach is:

  • We render the modal content with a "Loading" placeholder. We give the placeholder a wrapper element with id="dom-element-id".
  • When visitor opens the modal, we also fetch the content.
  • When the content is available, we replace the temporary "Loading" placeholder content with the real stuff.

All of this happens in the a .phtml template of our module or theme.

/** @var \Magento\Framework\Escaper $escaper */
/** @var \Hyva\Theme\Model\ViewModelRegistry $viewModels */
    function fetchModalContent() {
        let isContentLoaded = false
        return {
            loadContent() {
                if (isContentLoaded) return
                .then(response => response.text())
                .then(content => {
                    isContentLoaded = true
                    hyva.replaceDomElement('#dom-element-id', content)
<div x-data="{...hyva.modal(), ...fetchModalContent()}">
    <button type="button" class="text-center p-2 px-6 border border-secondary rounded"
            @click="show('the-modal', $event); loadContent()"
        <?= $escaper->escapeHtml(__('Open')) ?>
    <?= $modalViewModel->createModal()
        ->withContent("<div id='dom-element-id' class='mt-2'>{$escaper->escapeHtml(__('Loading products...'))}</div>")
        ->addDialogClass('border', 'border-1');

Things of note

  • The loadContent method is merged into the Alpine component defined by hyva.modal().
  • The example content DOM ID dom-element-id is specified in three places:
    1. In the initial modal content, to indicate the target element to receive the content.
    2. In the layout XML, on the modal content wrapper container, so it is present in the response content.
    3. As the first argument of the hyva.replaceDomElement call in the loadContent method.
  • The contents of the slider used for this example are declared in layout XML. See the slider documentation for more details.
  • Instead of declaring the modal content using withContent() as done in the example above, a separate .phtml template could be used, too.

Thanks to Adam from for contributing this tutorial!