Magento Series: Implementing a Widget

Examples used in this tutorial were implemented and tested in both the Magento2.4.3-p1 with Luma and ScandiPWA V5.2.6 themes. For an example of a demo widget you can use for your project, see Demo Widget ScandiPWA Extension. For this tutorial, you must be familiar with adding a widget to the Magento CMS system.

To start, prepare media content for the tutorial: 4 icons and one banner image.

Here are the designs we had as the basis for this tutorial:

Desktop
Mobile

What data is required to be manageable?

  • Banner image
  • Steps text and steps numbers can be hardcoded
  • Button text
  • Button link

The steps icons are not in the list of manageable fields.

Is a widget preview in the admin required?

If a preview is required, use the Magento PHTML rendering approach; otherwise, use the ScandiPWA components rendering approach.

In this tutorial, we’ll show both approaches.

Steps to implementing a widget in Magento 2

1. Create a new module

Create any missing folders and add the files. Adjust file content to suit your needs. 

project_root/app/code/ExampleVendor/ExampleModule/etc/module.xml

<?xml version="1.0"?>
<!--
/**
 * ExampleVendor_ExampleModule
 *
 * @category ExampleVendor
 * @package  ExampleVendor_ExampleModule
 * @author   Firstname Lastname <[email protected]>
 */
-->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="ExampleVendor_ExampleModule">
        <sequence>
            <module name="Magento_Widget"/>
        </sequence>
    </module>
</config>

project_root/app/code/ExampleVendor/ExampleModule/registration.php

<?php
/**
 * ExampleVendor_ExampleModule
 *
 * @category ExampleVendor
 * @package  ExampleVendor_ExampleModule
 * @author   Firstname Lastname <[email protected]>
 */

declare(strict_types=1);

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'ExampleVendor_ExampleModule',
__DIR__
);

Enable the module

From project_root, run the following command:

php bin/magento setup:upgrade

Run the command from the command line. See Available commands for more info on CMA setups. The module ExampleVendor_ExampleModule should appear in project_root/app/etc/config.php.

2. Add widget.xml

Please use snake case for parameter names, e.g., banner_title. To get data from this parameter, use $block->getBannerTitle()—this magic Magento function will get the data from the parameter name banner_title.

project_root/app/code/ExampleVendor/ExampleModule/etc/widget.xml

<?xml version="1.0"?>
<!--
/**
 * ExampleVendor_ExampleModule
 *
 * @category ExampleVendor
 * @package  ExampleVendor_ExampleModule
 * @author   Firstname Lastname <[email protected]>
 */
-->

<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
    <widget class="ExampleVendor\ExampleModule\Block\Widget\ExampleBanner" id="example_banner" placeholder_image="ExampleVendor_ExampleModule::example_banner_icon.png">
        <label>Example Banner</label>
        <description>Scandiweb Widget Tutorial</description>
        <parameters>
            <parameter name="banner_title" xsi:type="text" required="true" visible="true" sort_order="10">
                <label>Heading text</label>
                <value>Test</value>
            </parameter>
            <parameter name="step_1_text" xsi:type="text" required="true" visible="true" sort_order="30">
                <label>Step 1 Text</label>
            </parameter>
            <parameter name="step_2_text" xsi:type="text" required="true" visible="true" sort_order="50">
                <label>Step 2 Text</label>
            </parameter>
            <parameter name="step_3_text" xsi:type="text" required="true" visible="true" sort_order="70">
                <label>Step 3 Text</label>
            </parameter>
            <parameter name="step_4_text" xsi:type="text" required="true" visible="true" sort_order="90">
                <label>Step 4 Text</label>
            </parameter>
            <parameter name="button_text" xsi:type="text" required="true" visible="true" sort_order="100">
                <label>Button Text</label>
            </parameter>
            <parameter name="button_url" xsi:type="text" required="true" visible="true" sort_order="110">
                <label>Button URL</label>
                <description>Redirect URL on click</description>
            </parameter>
            <parameter name="banner_image" xsi:type="block" required="true" visible="true" sort_order="120">
                <label>Banner Image</label>
                <block class="ExampleVendor\ExampleModule\Block\Adminhtml\Widget\ImageChooser">
                    <data>
                        <item name="button" xsi:type="array">
                            <item name="open" xsi:type="string">Choose Image...</item>
                        </item>
                    </data>
                </block>
            </parameter>
        </parameters>
    </widget>
</widgets>

3. Add widget class

project_root/app/code/ExampleVendor/ExampleModule/Block/Widget/ExampleBanner.php

<?php
/**
 * ExampleVendor_ExampleModule
 *
 * @category ExampleVendor
 * @package  ExampleVendor_ExampleModule
 * @author   Firstname Lastname <[email protected]>
 */

declare(strict_types=1);

namespace ExampleVendor\ExampleModule\Block\Widget;

use Magento\Framework\View\Element\Template;
use Magento\Widget\Block\BlockInterface;

class ExampleBanner extends Template implements BlockInterface
{
    protected $_template = 'widget/example_banner.phtml';
}

4. Add widget placeholder image

Add a placeholder image to distinguish your widget from others in the admin.

The placeholder image is defined by this section placeholder_image="ExampleVendor_ExampleModule::example_banner_icon.png" in widget.xml, so place at least a 40×40 pixels large image in project_root/app/code/ExampleVendor/ExampleModule/view/adminhtml/web/example_banner_icon.png.

5. Add banner image chooser class

project_root/app/code/ExampleVendor/ExampleModule/Block/Adminhtml/Widget/ImageChooser.php

<?php
/**
 * ExampleVendor_ExampleModule
 *
 * @category ExampleVendor
 * @package  ExampleVendor_ExampleModule
 * @author   Firstname Lastname <[email protected]>
 */

declare(strict_types=1);

namespace ExampleVendor\ExampleModule\Block\Adminhtml\Widget;

use Magento\Framework\Data\Form\Element\AbstractElement;
use Magento\Framework\Data\Form\Element\Factory as ElementFactory;
use Magento\Backend\Block\Template;
use Magento\Backend\Block\Widget\Button;
use Magento\Backend\Block\Template\Context;
use Magento\Framework\Data\Form\Element\Text;
use Magento\Framework\Exception\LocalizedException;

class ImageChooser extends Template
{
    /**
     * @var ElementFactory
     */
    protected $elementFactory;

    /**
     * @param Context $context
     * @param ElementFactory $elementFactory
     * @param array $data
     */
    public function __construct(
        Context $context,
        ElementFactory $elementFactory,
        array $data = []
    ) {
        parent::__construct($context, $data);
        $this->elementFactory = $elementFactory;
    }

    /**
     * Prepare chooser element HTML
     *
     * @param AbstractElement $element
     *
     * @return AbstractElement
     * @throws LocalizedException
     */
    public function prepareElementHtml(AbstractElement $element)
    {
        $config = $this->_getData('config');
        $sourceUrl = $this->getUrl(
            'cms/wysiwyg_images/index',
            ['target_element_id' => $element->getId(), 'type' => 'file']
        );

        /** @var Button $chooser */
        $chooser = $this->getLayout()->createBlock(Button::class)
            ->setType('button')
            ->setClass('btn-chooser')
            ->setLabel($config['button']['open'])
            ->setOnClick('MediabrowserUtility.openDialog(\'' . $sourceUrl . '\', 0, 0, "MediaBrowser", {})')
            ->setDisabled($element->getReadonly());

        /** @var Text $input */
        $input = $this->elementFactory->create('text', ['data' => $element->getData()]);
        $input->setId($element->getId());
        $input->setForm($element->getForm());
        $input->setClass('widget-option input-text admin__control-text');
        if ($element->getRequired()) {
            $input->addClass('required-entry');
        }

        $element->setData('after_element_html', $input->getElementHtml() . $chooser->toHtml()
            . "<script>require(['mage/adminhtml/browser']);</script>");

        return $element;
    }
}

6. Add font and banner icons to the media directory

In this example, add icons manually to the project_root/pub/media/wysiwyg/example_banner directory and RobotoCondensed-Bold.woff2 font to the project_root/pub/media/font directory.

In most cases, you should migrate media content via migration scripts. 

7. Render content

You can use translation functions __('Some Text') in both rendering methods to make your widgets more flexible and easier to use.

ScandiPWA example with rendering via frontend

FYI, this is the recommended approach. Create a new component with a container via the ScandiPWA CLI utility or manually.

project_root/app/code/ExampleVendor/ExampleModule/etc/graphql/di.xml

<?xml version="1.0"?>
<!--
/**
 * ExampleVendor_ExampleModule
 *
 * @category ExampleVendor
 * @package  ExampleVendor_ExampleModule
 * @author   Firstname Lastname <[email protected]>
 */
-->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="ScandiPWA\CmsGraphQl\Model\Template\VirtualFilter"
                 type="ScandiPWA\CmsGraphQl\Model\Template\Filter">
        <arguments>
            <argument name="availableFilters" xsi:type="array">
                <item name="ExampleBanner" xsi:type="string">ExampleVendor\ExampleModule\Block\Widget\ExampleBanner</item>
            </argument>
        </arguments>
    </virtualType>
</config>

project_root/app/code/ExampleVendor/ExampleModule/view/frontend/templates/widget/example_banner.phtml

Pay attention to how the ScandiPWA .phtml template differs from the Magento2 example. In the ScandiPWA case widget tag is used to pass block class data as tag parameters to the React component.

<?php
/**
 * ExampleVendor_ExampleModule
 *
 * @category    ExampleVendor
 * @package     ExampleVendor_ExampleModule
 * @author      Firstname Lastname <[email protected]>
 */

/** @var $block ExampleVendor\ExampleModule\Block\Widget\ExampleBanner */
/** @var $escaper Magento\Framework\Escaper */
?>

<widget type="ExampleBanner"
        data-banner-image="<?= $escaper->escapeHtml($block->getBannerImage()) ?>"
        data-banner-title="<?= $escaper->escapeHtml($block->getBannerTitle()) ?>"
        data-step-1-text="<?= $escaper->escapeHtml($block->getStep1Text()) ?>"
        data-step-2-text="<?= $escaper->escapeHtml($block->getStep2Text()) ?>"
        data-step-3-text="<?= $escaper->escapeHtml($block->getStep3Text()) ?>"
        data-step-4-text="<?= $escaper->escapeHtml($block->getStep4Text()) ?>"
        data-button-url="<?= $escaper->escapeHtml($block->getButtonUrl()) ?>"
        data-button-text="<?= $escaper->escapeHtml($block->getButtonText()) ?>"
/>

project_root/pwa/src/component/ExampleBannerWidget/ExampleBannerWidget.container.js

In this example, the ScandiPWA theme folder is named pwa. If your theme folder is named differently, change the namespace first path part to your theme folder name with uppercase. Otherwise, the theme won’t build.

/* eslint-disable no-magic-numbers */
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { connect } from 'react-redux';

import ExampleBannerWidget from './ExampleBannerWidget.component';

/** @namespace Pwa/Component/ExampleBannerWidget/Container/mapStateToProps */
export const mapStateToProps = () => ({});

/** @namespace Pwa/Component/ExampleBannerWidget/Container/mapDispatchToProps */
export const mapDispatchToProps = () => ({});

/** @namespace Pwa/Component/ExampleBannerWidget/Container */
export class ExampleBannerWidgetContainer extends PureComponent {
    static propTypes = {
        'data-banner-image': PropTypes.string.isRequired,
        'data-banner-title': PropTypes.string.isRequired,
        'data-step-1-text': PropTypes.string.isRequired,
        'data-step-2-text': PropTypes.string.isRequired,
        'data-step-3-text': PropTypes.string.isRequired,
        'data-step-4-text': PropTypes.string.isRequired,
        'data-button-url': PropTypes.string.isRequired,
        'data-button-text': PropTypes.string.isRequired
    };

    descriptionSteps = [
        {
            text: '',
            icon: '/media/wysiwyg/example_banner/add-to-cart-icon.png'
        },
        {
            text: '',
            icon: '/media/wysiwyg/example_banner/select-store-icon.png'
        },
        {
            text: '',
            icon: '/media/wysiwyg/example_banner/pay-icon.png'
        },
        {
            text: '',
            icon: '/media/wysiwyg/example_banner/sms-icon.png'
        }
    ];

    __construct(props) {
        super.__construct(props);

        const {
            'data-step-1-text': step1Text,
            'data-step-2-text': step2Text,
            'data-step-3-text': step3Text,
            'data-step-4-text': step4Text
        } = this.props;

        this.descriptionSteps[0].text = step1Text;
        this.descriptionSteps[1].text = step2Text;
        this.descriptionSteps[2].text = step3Text;
        this.descriptionSteps[3].text = step4Text;
    }

    render() {
        const {
            'data-banner-image': bannerImage,
            'data-banner-title': bannerTitle,
            'data-button-url': buttonUrl,
            'data-button-text': buttonText
        } = this.props;

        return (
            <ExampleBannerWidget
              bannerImage={ bannerImage }
              bannerTitle={ bannerTitle }
              buttonUrl={ buttonUrl }
              buttonText={ buttonText }
              descriptionSteps={ this.descriptionSteps }
            />
        );
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(ExampleBannerWidgetContainer);

project_root/pwa/src/component/ExampleBannerWidget/ExampleBannerWidget.component.js

In this example, the ScandiPWA theme folder is named pwa. If your theme folder is named differently, change the namespace first path part to your theme folder name with uppercase. Otherwise, the theme won’t build.

import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import Link from 'Component/Link';

import './ExampleBannerWidget.style';

/** @namespace Pwa/Component/ExampleBannerWidget/Component */
export class ExampleBannerWidgetComponent extends PureComponent {
    static propTypes = {
        bannerImage: PropTypes.string.isRequired,
        bannerTitle: PropTypes.string.isRequired,
        buttonUrl: PropTypes.string.isRequired,
        buttonText: PropTypes.string.isRequired,
        descriptionSteps: PropTypes.arrayOf(
            PropTypes.shape({
                text: PropTypes.string.isRequired,
                icon: PropTypes.string.isRequired
            }).isRequired
        ).isRequired
    };

    renderBannerImage() {
        const {
            bannerImage
        } = this.props;

        return (
            <div block="ExampleBanner" elem="BannerImage">
                <img src={ bannerImage } alt={ bannerImage } />
            </div>
        );
    }

    renderDescriptionTitle() {
        const {
            bannerTitle
        } = this.props;

        return (
            <div block="ExampleBanner" elem="DescriptionTitle">
                <span>{ bannerTitle }</span>
            </div>
        );
    }

    renderDescriptionSteps() {
        const {
            descriptionSteps
        } = this.props;

        return (
            <div block="ExampleBanner" elem="DescriptionSteps">
                { descriptionSteps.map(({ text, icon }, index) => {
                    const stepNumberText = `${ index + 1 }.`;

                    return (
                        <div block="ExampleBanner" elem="DescriptionStepWrapper" key={ stepNumberText }>
                            <div block="ExampleBanner" elem="DescriptionStepContent">
                                <span block="ExampleBanner" elem="Icon">
                                    <img src={ icon } alt={ icon } />
                                </span>
                                <span block="ExampleBanner" elem="DescriptionStepText">
                                    <span block="ExampleBanner" elem="DescriptionStepText" mods={ { number: true } }>
                                        { stepNumberText }
                                    </span>
                                    <span>{ text }</span>
                                </span>
                            </div>
                        </div>
                    );
                }) }
            </div>
        );
    }

    renderDescriptionActions() {
        const {
            buttonUrl,
            buttonText
        } = this.props;

        return (
            <div block="ExampleBanner" elem="DescriptionActions">
                <Link to={ buttonUrl } block="Button">
                    <span>{ buttonText }</span>
                </Link>
            </div>
        );
    }

    renderBannerDescription() {
        return (
            <div block="ExampleBanner" elem="Description">
                { this.renderDescriptionTitle() }
                { this.renderDescriptionSteps() }
                { this.renderDescriptionActions() }
            </div>
        );
    }

    render() {
        return (
            <div block="ExampleBanner">
                { this.renderBannerImage() }
                { this.renderBannerDescription() }
            </div>
        );
    }
}

export default ExampleBannerWidgetComponent;

project_root/pwa/src/component/ExampleBannerWidget/index.js

export { default } from './ExampleBannerWidget.container';

project_root/pwa/src/component/ExampleBannerWidget/ExampleBannerWidget.style.scss

@font-face {
    font-family: 'RobotoCondensed';
    font-weight: bold;
    font-display: fallback;
    src: local("RobotoCondensed Bold"), local("RobotoCondensed-Bold"), url('/../../pub/media/font/RobotoCondensed-Bold.woff2') format("woff2");
}

:root {
    --max-example-banner-width: 1380px;
}

a {
    &:active, &:focus, &:visited {
        color: inherit;
    }
}

.ExampleBanner {
    display: flex;
    -webkit-flex-direction: column;
    flex-direction: column;
    overflow: hidden;
    max-width: var(--max-example-banner-width);
    margin: 40px 15px;
    background-color: #f9f9f9;
    position: relative;

    span {
        font-family: RobotoCondensed, sans-serif;
    }

    &-Description {
        padding: 20px 10px;

        &Title {
            font-family: RobotoCondensed, sans-serif;
            font-size: 24px;
            font-weight: 700;
            line-height: 1;
        }

        &Steps {
            display: flex;
            margin: 15px 0;
            width: 100%;
            -webkit-flex-direction: column;
            flex-direction: column;
            z-index: 2;
            position: relative;
        }

        &StepWrapper {
            margin: 5px 0;
            display: flex;
            -webkit-align-items: center;
            align-items: center;
        }

        &StepContent {
            display: flex;
            -webkit-align-content: center;
            align-content: center;
            -webkit-align-items: center;
            align-items: center;
            background: #f9f9f9;
            border: 1px solid #a6a6a6;
            width: 100%;
        }

        &StepText {
            margin: 7px 5px 7px 0;
            font-family: RobotoCondensed, sans-serif;
            font-weight: 700;
            font-size: 16px;
            line-height: 24px;
        }

        &Actions {
            .Button {
                text-decoration: none;
                display: inline-block;
                border-radius: 3px;
                font-weight: 700;
                text-align: center;
                text-transform: uppercase;
                color: #fff;
                transition-property: background-color,color,border;
                will-change: background-color,color,border;
                transition-timing-function: ease-out;
                transition-duration: .25s;
                cursor: pointer;
                border: 1px solid #000;
                background: #000;
                padding: 12px 0;
                font-size: 16px;
                font-family: RobotoCondensed, sans-serif;
                width: 100%;

                &:hover {
                    text-decoration: none;
                    color: #fff;
                    border-color: #c70000;
                    background-color: #c70000;
                }
            }
        }
    }

    &-Icon {
        padding: 6px 15px;

        img {
            width: 32px;
            height: 32px;
            max-width: none;
        }
    }

    &-BannerImage {
        max-height: 300px;
        overflow: hidden;
        object-fit: cover;
        z-index: 1;
    }
}

@media (min-width: 1024px) {
    .ExampleBanner {
        background-color: initial;
        -webkit-flex-direction: row-reverse;
        flex-direction: row-reverse;
        margin: 40px auto;

        &-Description {
            background-color: #f9f9f9;
            padding: 45px 40px;
            width: 100%;

            &Title {
                padding-block-end: 10px;
                max-width: 38%;
                font-size: 32px;
            }

            &Steps {
                -webkit-flex-direction: row;
                flex-direction: row;
            }

            &StepWrapper {
                margin: 0 5px;
                text-align: center;

                &:first-child {
                    margin-inline-start: 0;
                }

                span {
                    display: block;
                }
            }

            &StepContent {
                width: 198px;
                height: 100%;
                -webkit-flex-direction: column;
                flex-direction: column;
                padding: 15px 0 25px;
            }

            &StepText {
                margin: 0;
                padding: 0 15px;
                font-size: 20px;

                &_number {
                    margin: 0 0 7px;
                }
            }

            &Actions {
                padding-block-start: 20px;

                .Button {
                    width: auto;
                    padding: 12px 36px;
                    border-radius: 6px;

                    &:hover {
                        padding: 12px 36px;
                        border-radius: 6px;
                    }
                }
            }
        }

        &-Icon {
            padding: 15px;

            img {
                width: 40px;
                height: 40px;
            }
        }

        &-BannerImage {
            margin: 0 15px;
            position: absolute;
            max-height: none;
            width: 60%;
            height: 100%;
            padding: 0;
            background-color: initial;
        }
    }
}

project_root/pwa/src/plugin/WidgetFactory.plugin.js

This plugin is required to pass widget attributes to the Widget component. If your ScandiPWA version does not support the plugin system, override the Widget component.

/* eslint-disable @scandipwa/scandipwa-guidelines/jsx-no-props-destruction */
import ExampleBannerWidget from 'Component/ExampleBannerWidget/ExampleBannerWidget.container';
import RenderWhenVisible from 'Component/RenderWhenVisible';

const renderMap = (member) => ({
    ...member,
    ExampleBanner: {
        component: ExampleBannerWidget
    }
});

const renderContent = (args, callback, instance) => {
    const { type } = instance.props;

    const {
        component: Widget,
        fallback
    } = instance.renderMap[type] || {};

    if (Widget !== undefined) {
        return (
            <RenderWhenVisible fallback={ fallback }>
                <Widget { ...instance.props } />
            </RenderWhenVisible>
        );
    }

    return null;
};

export default {
    'Component/WidgetFactory/Component': {
        'member-function': {
            renderContent
        },
        'member-property': {
            renderMap
        }
    }
};

Make sure that theme is rebuilt.

Magento2 example with rendering via backend

Use this approach if it is required to preview the widget in the admin panel, or styles should be loaded on demand; otherwise, see the ScandiPWA example.

To render media directives that will be used in the widget to display the banner, enable the following configuration: Admin -> Stores -> Configuration -> General -> Content Management -> WYSIWYG Options -> Use Static URLs for Media Content in WYSIWYG

project_root/app/code/ExampleVendor/ExampleModule/view/frontend/templates/widget/example_banner.phtml

<?php
/**
 * ExampleVendor_ExampleModule
 *
 * @category    ExampleVendor
 * @package     ExampleVendor_ExampleModule
 * @author      Firstname Lastname <[email protected]>
 */

/** @var $block ExampleVendor\ExampleModule\Block\Widget\ExampleBanner */
/** @var $escaper Magento\Framework\Escaper */
?>

<style>
    @font-face {
        font-family: 'RobotoCondensed';
        font-weight: bold;
        font-display: fallback;
        src: local('RobotoCondensed Bold'), local('RobotoCondensed-Bold'), url('media/font/RobotoCondensed-Bold.woff2') format('woff2');
    }

    :root {
        --max-example-banner-width: 1380px;
    }

    a:active, a:focus, a:visited {
        color: inherit;
    }

    .ExampleBanner {
        display: flex;
        -webkit-flex-direction: column;
        flex-direction: column;
        overflow: hidden;
        max-width: var(--max-example-banner-width);
        margin: 40px 15px;
        background-color: #f9f9f9;
        position: relative;
    }

    .ExampleBanner span {
        font-family: RobotoCondensed, sans-serif !important;
    }

    @media (min-width: 1024px) {
        .ExampleBanner {
            background-color: initial;
            -webkit-flex-direction: row-reverse;
            flex-direction: row-reverse;
            margin: 40px auto;
        }
    }

    .ExampleBanner-Description {
        padding: 20px 10px;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-Description {
            background-color: #f9f9f9;
            padding: 45px 40px;
        }
    }

    .ExampleBanner-DescriptionTitle {
        font-family: RobotoCondensed, sans-serif;
        font-size: 24px;
        font-weight: 700;
        line-height: 1;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionTitle {
            padding-bottom: 10px;
            max-width: 38%;
            font-size: 32px;
        }
    }

    .ExampleBanner-DescriptionSteps {
        display: flex;
        margin: 15px 0;
        width: 100%;
        -webkit-flex-direction: column;
        flex-direction: column;
        z-index: 2;
        position: relative;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionSteps {
            -webkit-flex-direction: row;
            flex-direction: row;
        }
    }

    .ExampleBanner-DescriptionStepWrapper {
        margin: 5px 0;
        display: flex;
        -webkit-align-items: center;
        align-items: center;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionStepWrapper {
            margin: 0 5px;
            text-align: center;
        }

        .ExampleBanner-DescriptionStepWrapper:first-child {
            margin-left: 0;
        }

        .ExampleBanner-DescriptionStepWrapper span {
            display: block;
        }
    }

    .ExampleBanner-DescriptionStepContent {
        display: flex;
        -webkit-align-content: center;
        align-content: center;
        -webkit-align-items: center;
        align-items: center;
        background: #f9f9f9;
        border: 1px solid #a6a6a6;
        width: 100%;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionStepContent {
            width: 198px;
            height: 100%;
            -webkit-flex-direction: column;
            flex-direction: column;
            padding: 15px 0 5px;
        }
    }

    .ExampleBanner-DescriptionStepText {
        margin: 7px 20px 7px 0;
        font-family: RobotoCondensed, sans-serif;
        font-weight: 700;
        font-size: 16px;
        line-height: 24px;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionStepText_number {
            margin: 0 0 7px;
        }
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionStepText {
            margin: 0;
            padding: 0 15px;
            font-size: 20px;
        }
    }

    .ExampleBanner-DescriptionActions .Button {
        text-decoration: none;
        display: inline-block;
        border-radius: 3px;
        font-weight: 700;
        text-align: center;
        text-transform: uppercase;
        color: #fff;
        transition-property: background-color,color,border;
        will-change: background-color,color,border;
        transition-timing-function: ease-out;
        transition-duration: .25s;
        cursor: pointer;
        border: 1px solid #000;
        background: #000;
        padding: 12px 0;
        font-size: 16px;
        font-family: RobotoCondensed, sans-serif;
        width: 100%;
    }

    .ExampleBanner-DescriptionActions .Button:hover {
        text-decoration: none;
        color: #fff;
        border-color: #c70000;
        background-color: #c70000;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionActions .Button {
            width: auto;
            padding: 12px 36px;
            border-radius: 6px;
        }

        .ExampleBanner-DescriptionActions .Button:hover {
            padding: 12px 36px;
            border-radius: 6px;
        }
    }

    @media (min-width: 1024px) {
        .ExampleBanner-DescriptionActions {
            padding-top: 20px;
        }
    }

    @media (min-width: 1024px) {
        .ExampleBanner-Description {
            width: 100%;
        }
    }

    .ExampleBanner-Icon {
        padding: 6px 15px;
    }

    .ExampleBanner-Icon img {
        width: 32px;
        height: 32px;
        max-width: none;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-Icon img {
            width: 40px;
            height: 40px;
        }
    }

    @media (min-width: 1024px) {
        .ExampleBanner-Icon {
            padding: 15px;
        }
    }

    .ExampleBanner-BannerImage {
        max-height: 300px;
        overflow: hidden;
        object-fit: cover;
        z-index: 1;
    }

    @media (min-width: 1024px) {
        .ExampleBanner-BannerImage {
            margin: 0 15px;
            position: absolute;
            max-height: none;
            width: 60%;
            height: 100%;
            padding: 0;
            background-color: initial;
        }
    }
</style>
<div class="ExampleBanner">
    <div class="ExampleBanner-BannerImage">
        <img src="<?= $escaper->escapeHtml($block->getBannerImage()) ?>" />
    </div>
    <div class="ExampleBanner-Description">
        <div class="ExampleBanner-DescriptionTitle">
            <span><?= $escaper->escapeHtml($block->getBannerTitle()) ?></span>
        </div>
        <div class="ExampleBanner-DescriptionSteps">
            <div class="ExampleBanner-DescriptionStepWrapper">
                <div class="ExampleBanner-DescriptionStepContent">
                    <span class="ExampleBanner-Icon">
                        <img src="/media/wysiwyg/example_banner/add-to-cart-icon.png">
                    </span>
                    <span class="ExampleBanner-DescriptionStepText">
                        <span class="ExampleBanner-DescriptionStepText_number">1.</span>
                        <span><?= $escaper->escapeHtml($block->getStep1Text()) ?></span>
                    </span>
                </div>
            </div>
            <div class="ExampleBanner-DescriptionStepWrapper">
                <div class="ExampleBanner-DescriptionStepContent">
                    <span class="ExampleBanner-Icon">
                        <img src="/media/wysiwyg/example_banner/select-store-icon.png">
                    </span>
                    <span class="ExampleBanner-DescriptionStepText">
                        <span class="ExampleBanner-DescriptionStepText_number">2.</span>
                        <span><?= $escaper->escapeHtml($block->getStep2Text()) ?></span>
                    </span>
                </div>
            </div>
            <div class="ExampleBanner-DescriptionStepWrapper">
                <div class="ExampleBanner-DescriptionStepContent">
                    <span class="ExampleBanner-Icon">
                        <img src="/media/wysiwyg/example_banner/pay-icon.png">
                    </span>
                    <span class="ExampleBanner-DescriptionStepText">
                        <span class="ExampleBanner-DescriptionStepText_number">3.</span>
                        <span><?= $escaper->escapeHtml($block->getStep3Text()) ?></span>
                    </span>
                </div>
            </div>
            <div class="ExampleBanner-DescriptionStepWrapper">
                <div class="ExampleBanner-DescriptionStepContent">
                    <span class="ExampleBanner-Icon">
                        <img src="/media/wysiwyg/example_banner/sms-icon.png">
                    </span>
                    <span class="ExampleBanner-DescriptionStepText">
                        <span class="ExampleBanner-DescriptionStepText_number">4.</span>
                        <span><?= $escaper->escapeHtml($block->getStep4Text()) ?></span>
                    </span>
                </div>
            </div>
        </div>
        <div class="ExampleBanner-DescriptionActions">
            <a href="<?= $escaper->escapeHtml($block->getButtonUrl()) ?>" class="Button">
                <span><?= $escaper->escapeHtml($block->getButtonText()) ?></span>
            </a>
        </div>
    </div>
</div>

Clean the Magento cache after adding the code.

8. Add the widget to a CMS page

Create the CMS page and drag the Text element to the page. If you’re updating the existing page, you need to add a row element first.

Click on the added element on the page, and click on the Insert Widget button.

Configure the widget.

Save the widget configurations and the page.

9. Test the CMS page on the frontend

Open your CMS page, and see the widget rendered on the CMS page.

ScandiPWA example with rendering via frontend

Desktop
Mobile

Magento 2 example with rendering via backend

Desktop
Mobile

Was this article helpful? For more Magento (Adobe Commerce) tips and tutorials from our certified developers, browse the Magento category in scandiweb blog, or contact our team directly!

If you enjoyed this post, you may also like