| name | magento2-widget-creation |
| description | Comprehensive guide for creating custom widget modules in Magento 2 that can be inserted into CMS pages and blocks. Covers module structure, widget configuration, templates, JavaScript, CSS, and form submission handling for non-Hyvä themes. |
Magento 2 Widget Creation for CMS Pages
Purpose
This skill provides comprehensive guidance on creating custom widget modules in Magento 2 (standard Luma/Blank themes, not Hyvä-based) that can be inserted into CMS pages, CMS blocks, or any content area using the widget system.
When to Use This Skill
Use this skill when you need to:
- Create a reusable component that can be inserted into CMS pages
- Build interactive elements (buttons, forms, modals) for content editors
- Develop custom functionality that non-technical users can add to pages
- Create widgets with configurable parameters that appear in the admin panel
- Implement widgets that work with standard Magento themes (Luma/Blank)
Do NOT use this skill for:
- Hyvä theme widgets (use hyva-tailwind-integration skill instead)
- Backend admin widgets
- UI components or admin grids
Prerequisites
- Existing vendor namespace or willingness to create one
- Basic understanding of Magento 2 module structure
- Knowledge of XML configuration
- Familiarity with Magento templates and blocks
- Understanding of JavaScript widget pattern (optional, for interactive widgets)
Widget Module Structure
A complete widget module requires these components:
app/code/Vendor/ModuleName/
├── registration.php # Module registration
├── etc/
│ ├── module.xml # Module configuration
│ ├── widget.xml # Widget definition
│ ├── email_templates.xml # (Optional) Email templates
│ └── frontend/
│ └── routes.xml # (Optional) For form submissions
├── Block/
│ └── Widget/
│ └── WidgetName.php # Widget block class
├── Controller/ # (Optional) For form handlers
│ └── Index/
│ └── Submit.php
└── view/frontend/
├── templates/
│ └── widget/
│ └── template.phtml # Widget template
├── layout/
│ └── default.xml # (Optional) Load CSS/JS globally
├── web/
│ ├── js/
│ │ └── widget-script.js # (Optional) Custom JS
│ └── css/
│ └── widget-style.css # (Optional) Custom CSS
├── requirejs-config.js # (Optional) JS module mapping
└── email/ # (Optional) Email templates
└── template.html
Step-by-Step Widget Creation
Step 1: Create Module Registration
File: registration.php
<?php
/**
* Copyright © Vendor. All rights reserved.
*/
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_ModuleName',
__DIR__
);
Step 2: Create Module Configuration
File: etc/module.xml
<?xml version="1.0"?>
<!--
/**
* Copyright © Vendor. All rights reserved.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_ModuleName" setup_version="1.0.0">
<sequence>
<module name="Magento_Cms"/>
<module name="Magento_Widget"/>
</sequence>
</module>
</config>
Key Points:
setup_versionis legacy but still commonly used- Add dependencies in
<sequence>-Magento_CmsandMagento_Widgetare required for widgets - Add other modules your widget depends on (e.g.,
Magento_Email,Magento_Catalog)
Step 3: Create Widget Configuration
File: etc/widget.xml
This is where you define your widget's metadata and configurable parameters.
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright © Vendor. All rights reserved.
*/
-->
<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
<widget id="widget_unique_id" class="Vendor\ModuleName\Block\Widget\WidgetName">
<label translate="true">Widget Display Name</label>
<description translate="true">Brief description of what the widget does</description>
<parameters>
<!-- Text Parameter -->
<parameter name="text_param" xsi:type="text" required="false" visible="true">
<label translate="true">Text Label</label>
<description translate="true">Description shown in admin</description>
</parameter>
<!-- Select/Dropdown Parameter -->
<parameter name="select_param" xsi:type="select" required="false" visible="true">
<label translate="true">Select Option</label>
<options>
<option name="option1" value="value1">
<label>Option 1</label>
</option>
<option name="option2" value="value2">
<label>Option 2</label>
</option>
</options>
</parameter>
<!-- Yes/No Parameter -->
<parameter name="enabled" xsi:type="select" required="false" visible="true"
source_model="Magento\Config\Model\Config\Source\Yesno">
<label translate="true">Enable Feature</label>
</parameter>
<!-- CMS Block Chooser -->
<parameter name="block_id" xsi:type="block" visible="true" required="false">
<label translate="true">CMS Block</label>
<block class="Magento\Cms\Block\Adminhtml\Block\Widget\Chooser">
<data>
<item name="button" xsi:type="array">
<item name="open" xsi:type="string">Select Block...</item>
</item>
</data>
</block>
</parameter>
<!-- Category Chooser -->
<parameter name="category_id" xsi:type="select" visible="true"
source_model="Magento\Catalog\Model\Category\Attribute\Source\Categories">
<label translate="true">Category</label>
</parameter>
</parameters>
</widget>
</widgets>
Widget Parameter Types:
text- Simple text inputselect- Dropdown selectionmultiselect- Multiple selectionsblock- CMS block pickerpage- CMS page pickerconditions- Product/category conditions (advanced)
Important Attributes:
id- Unique identifier for the widgetclass- Full namespaced path to your block classrequired- Whether parameter is mandatoryvisible- Whether parameter shows in admin
Step 4: Create Block Class
File: Block/Widget/WidgetName.php
The block class handles the widget's logic and data.
<?php
/**
* Copyright © Vendor. All rights reserved.
*/
declare(strict_types=1);
namespace Vendor\ModuleName\Block\Widget;
use Magento\Framework\View\Element\Template;
use Magento\Widget\Block\BlockInterface;
use Magento\Framework\View\Element\Template\Context;
class WidgetName extends Template implements BlockInterface
{
/**
* Template path relative to view/frontend/templates/
*
* @var string
*/
protected $_template = 'Vendor_ModuleName::widget/template.phtml';
/**
* @param Context $context
* @param array $data
*/
public function __construct(
Context $context,
array $data = []
) {
parent::__construct($context, $data);
}
/**
* Get widget parameter with default value
*
* @return string
*/
public function getParameterValue(): string
{
return $this->getData('text_param') ?: 'default value';
}
/**
* Get widget select parameter
*
* @return string
*/
public function getSelectValue(): string
{
return $this->getData('select_param') ?: 'value1';
}
/**
* Check if feature is enabled
*
* @return bool
*/
public function isEnabled(): bool
{
return (bool)$this->getData('enabled');
}
/**
* Get URL for AJAX or form submission
*
* @return string
*/
public function getActionUrl(): string
{
return $this->getUrl('modulename/index/submit');
}
}
Best Practices:
- Always
declare(strict_types=1); - Implement
BlockInterface - Use
$this->getData('param_name')to access widget parameters - Provide default values with
?:operator - Add type hints and return types
- Keep business logic out of templates - put it in block methods
Step 5: Create Template File
File: view/frontend/templates/widget/template.phtml
Templates render the HTML output. Always escape data for security.
<?php
/**
* Copyright © Vendor. All rights reserved.
*
* @var $block Vendor\ModuleName\Block\Widget\WidgetName
* @var $escaper Magento\Framework\Escaper
*/
$paramValue = $block->getParameterValue();
$selectValue = $block->getSelectValue();
$isEnabled = $block->isEnabled();
$uniqueId = uniqid('widget_');
?>
<?php if ($isEnabled): ?>
<div class="custom-widget" id="<?= $escaper->escapeHtmlAttr($uniqueId) ?>">
<div class="widget-content">
<h3><?= $escaper->escapeHtml(__('Widget Title')) ?></h3>
<p><?= $escaper->escapeHtml($paramValue) ?></p>
<?php if ($selectValue === 'value1'): ?>
<div class="option-one-content">
<!-- Content for option 1 -->
</div>
<?php endif; ?>
<button type="button"
class="action primary widget-button"
data-mage-init='{"widgetName": {"elementId": "<?= $escaper->escapeJs($uniqueId) ?>"}}'>
<?= $escaper->escapeHtml(__('Click Me')) ?>
</button>
</div>
</div>
<?php endif; ?>
Template Best Practices:
- Always use
$escaper->escapeHtml()for text content - Use
$escaper->escapeHtmlAttr()for HTML attributes - Use
$escaper->escapeJs()for JavaScript strings - Use
$escaper->escapeUrl()for URLs - Use
__()for translatable strings - Generate unique IDs to avoid conflicts (use
uniqid()) - Add proper
@varcomments for IDE support
Common Escaping Methods:
// Text content
<?= $escaper->escapeHtml($text) ?>
// HTML attributes
<div class="<?= $escaper->escapeHtmlAttr($class) ?>">
// JavaScript strings
data-value="<?= $escaper->escapeJs($value) ?>"
// URLs
<a href="<?= $escaper->escapeUrl($url) ?>">
// CSS
<div style="<?= $escaper->escapeCss($style) ?>">
Step 6: Add JavaScript (Optional)
If your widget needs interactivity, add JavaScript using Magento's widget pattern.
File: view/frontend/requirejs-config.js
/**
* Copyright © Vendor. All rights reserved.
*/
var config = {
map: {
'*': {
widgetName: 'Vendor_ModuleName/js/widget-script'
}
}
};
File: view/frontend/web/js/widget-script.js
/**
* Copyright © Vendor. All rights reserved.
*/
define([
'jquery',
'jquery-ui-modules/widget'
], function ($) {
'use strict';
/**
* Widget initialization pattern
*/
$.widget('vendor.widgetName', {
options: {
elementId: '',
ajaxUrl: ''
},
/**
* Widget creation
* @private
*/
_create: function () {
this._bind();
},
/**
* Bind event handlers
* @private
*/
_bind: function () {
var self = this;
this.element.on('click', function (e) {
e.preventDefault();
self._handleClick();
});
},
/**
* Handle click event
* @private
*/
_handleClick: function () {
console.log('Widget clicked!');
console.log('Element ID:', this.options.elementId);
// Example AJAX call
if (this.options.ajaxUrl) {
this._makeAjaxRequest();
}
},
/**
* Make AJAX request
* @private
*/
_makeAjaxRequest: function () {
var self = this;
$.ajax({
url: this.options.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
// Your data here
},
success: function (response) {
self._handleResponse(response);
},
error: function () {
console.error('Request failed');
}
});
},
/**
* Handle AJAX response
* @private
*/
_handleResponse: function (response) {
if (response.success) {
console.log('Success:', response.message);
} else {
console.error('Error:', response.message);
}
}
});
return $.vendor.widgetName;
});
Initialization in Template:
<button type="button"
data-mage-init='{"widgetName": {
"elementId": "<?= $escaper->escapeJs($uniqueId) ?>",
"ajaxUrl": "<?= $escaper->escapeUrl($block->getActionUrl()) ?>"
}}'>
Click Me
</button>
Alternative Initialization with data-bind (Knockout.js):
<!-- For Knockout.js integration -->
<div data-bind="scope: 'widget-scope'">
<!-- content -->
</div>
<script type="text/x-magento-init">
{
"[data-bind]": {
"Magento_Ui/js/core/app": {
"components": {
"widget-scope": {
"component": "Vendor_ModuleName/js/widget-component"
}
}
}
}
}
</script>
Step 7: Add CSS Styling (Optional)
File: view/frontend/layout/default.xml
Load your CSS globally across all pages.
<?xml version="1.0"?>
<!--
/**
* Copyright © Vendor. All rights reserved.
*/
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<css src="Vendor_ModuleName::css/widget-style.css"/>
</head>
</page>
File: view/frontend/web/css/widget-style.css
/**
* Copyright © Vendor. All rights reserved.
*/
/* Widget Container */
.custom-widget {
padding: 20px;
margin: 10px 0;
}
.custom-widget .widget-content {
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
}
.custom-widget h3 {
margin: 0 0 10px;
font-size: 18px;
font-weight: 600;
}
.custom-widget .widget-button {
margin-top: 10px;
}
/* Responsive Design */
@media (max-width: 768px) {
.custom-widget {
padding: 15px;
}
.custom-widget .widget-content {
padding: 10px;
}
}
CSS Best Practices:
- Use specific class names to avoid conflicts
- Follow mobile-first approach with media queries
- Respect existing theme styles
- Use CSS variables for theming (if supported)
- Avoid
!importantunless absolutely necessary
Advanced: Adding Controllers for Form Submission
For widgets that need to process data (forms, AJAX requests), add a controller.
Step 1: Create Frontend Routes
File: etc/frontend/routes.xml
<?xml version="1.0"?>
<!--
/**
* Copyright © Vendor. All rights reserved.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="modulename" frontName="modulename">
<module name="Vendor_ModuleName" />
</route>
</router>
</config>
Route URL Pattern:
- URL:
https://yourstore.com/modulename/index/submit modulename= frontNameindex= controller directorysubmit= action file name
Step 2: Create Controller Action
File: Controller/Index/Submit.php
<?php
/**
* Copyright © Vendor. All rights reserved.
*/
declare(strict_types=1);
namespace Vendor\ModuleName\Controller\Index;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Exception\LocalizedException;
use Psr\Log\LoggerInterface;
/**
* Handle form submission
*/
class Submit implements HttpPostActionInterface
{
/**
* @var RequestInterface
*/
private $request;
/**
* @var JsonFactory
*/
private $resultJsonFactory;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @param RequestInterface $request
* @param JsonFactory $resultJsonFactory
* @param LoggerInterface $logger
*/
public function __construct(
RequestInterface $request,
JsonFactory $resultJsonFactory,
LoggerInterface $logger
) {
$this->request = $request;
$this->resultJsonFactory = $resultJsonFactory;
$this->logger = $logger;
}
/**
* Execute action
*
* @return \Magento\Framework\Controller\Result\Json
*/
public function execute()
{
$resultJson = $this->resultJsonFactory->create();
if (!$this->request->isPost()) {
return $resultJson->setData([
'success' => false,
'message' => __('Invalid request method.')
]);
}
try {
$postData = $this->request->getPostValue();
// Validate data
$this->validateData($postData);
// Process your data here
// Example: Save to database, send email, etc.
return $resultJson->setData([
'success' => true,
'message' => __('Your request has been submitted successfully.')
]);
} catch (LocalizedException $e) {
$this->logger->error('Widget form error: ' . $e->getMessage());
return $resultJson->setData([
'success' => false,
'message' => $e->getMessage()
]);
} catch (\Exception $e) {
$this->logger->error('Widget form error: ' . $e->getMessage());
return $resultJson->setData([
'success' => false,
'message' => __('An error occurred. Please try again later.')
]);
}
}
/**
* Validate form data
*
* @param array $data
* @throws LocalizedException
*/
private function validateData(array $data): void
{
if (empty($data['field_name'])) {
throw new LocalizedException(__('Field name is required.'));
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new LocalizedException(__('Please enter a valid email address.'));
}
}
}
Controller Best Practices:
- Use
HttpPostActionInterfacefor POST requests - Use
HttpGetActionInterfacefor GET requests - Return proper result objects (
JsonFactory,PageFactory,RedirectFactory) - Always validate input data
- Use try-catch blocks for error handling
- Log errors for debugging
- Return user-friendly error messages
Advanced: Modal Popup Widget
For complex interactions like modal forms:
Template with Modal:
<?php
$modalId = 'modal-' . uniqid();
?>
<div class="widget-container">
<button type="button"
class="action primary"
data-mage-init='{"widgetModal": {"modalId": "<?= $escaper->escapeJs($modalId) ?>"}}'>
<?= $escaper->escapeHtml(__('Open Modal')) ?>
</button>
</div>
<!-- Modal Structure -->
<div id="<?= $escaper->escapeHtmlAttr($modalId) ?>"
class="widget-modal"
style="display: none;"
data-role="widget-modal">
<div class="widget-modal-overlay"></div>
<div class="widget-modal-content">
<div class="widget-modal-header">
<h2><?= $escaper->escapeHtml(__('Modal Title')) ?></h2>
<button type="button" class="widget-modal-close" aria-label="Close">
<span>×</span>
</button>
</div>
<div class="widget-modal-body">
<form id="widget-form" method="post">
<!-- Form fields -->
<input type="text" name="field_name" required />
<button type="submit" class="action primary">
<?= $escaper->escapeHtml(__('Submit')) ?>
</button>
</form>
</div>
</div>
</div>
Modal JavaScript:
IMPORTANT:
- Do NOT use Magento's
Magento_Ui/js/modal/modalcomponent when you have custom modal HTML structure - it creates conflicts with z-index, positioning, and double overlays - ALWAYS move the modal element to
<body>on initialization to prevent parent container constraints (overflow, positioning, z-index)
define([
'jquery'
], function ($) {
'use strict';
$.widget('vendor.widgetModal', {
options: {
modalId: ''
},
_create: function () {
this._moveModalToBody();
this._bind();
},
/**
* Move modal element to body to prevent parent container constraints
* This is CRITICAL - without this, modal will be trapped inside widget container
*/
_moveModalToBody: function () {
var modalElement = $('#' + this.options.modalId);
if (modalElement.length && modalElement.parent()[0].tagName !== 'BODY') {
// Move modal to body so it's not constrained by parent positioning
modalElement.appendTo('body');
}
},
_bind: function () {
var self = this;
// Open modal on button click
this.element.on('click', function (e) {
e.preventDefault();
self.openModal();
});
},
openModal: function () {
var modalElement = $('#' + this.options.modalId);
if (modalElement.length) {
// Show modal with fade effect
modalElement.fadeIn(300);
$('body').addClass('modal-open');
// Bind close button (only once)
modalElement.find('.widget-modal-close, .action.cancel').off('click').on('click', function (e) {
e.preventDefault();
modalElement.fadeOut(300);
$('body').removeClass('modal-open');
});
// Bind overlay click (only once)
modalElement.find('.widget-modal-overlay').off('click').on('click', function (e) {
e.preventDefault();
modalElement.fadeOut(300);
$('body').removeClass('modal-open');
});
// Bind ESC key
$(document).off('keyup.widgetModal').on('keyup.widgetModal', function (e) {
if (e.key === 'Escape' || e.keyCode === 27) {
modalElement.fadeOut(300);
$('body').removeClass('modal-open');
$(document).off('keyup.widgetModal');
}
});
}
}
});
return $.vendor.widgetModal;
});
Modal CSS Styling:
/**
* Modal Styling
* IMPORTANT:
* - Use very high z-index (999999) with !important to ensure modal appears above all content
* - Many themes use high z-index values for headers, menus, etc. (10000+)
* - Modal container and all children use position: fixed to escape parent containers
* - Overlay uses darker background (0.7 opacity) to clearly indicate blocked content
*/
.widget-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999 !important;
}
.widget-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 999998 !important;
cursor: pointer;
}
.widget-modal-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
z-index: 999999 !important;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
/* Prevent body scroll when modal is open */
body.modal-open {
overflow: hidden;
}
/* Ensure modal container blocks all pointer events to elements below */
.widget-modal {
pointer-events: auto;
}
.widget-modal * {
pointer-events: auto;
}
.widget-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
border-bottom: 1px solid #e0e0e0;
}
.widget-modal-header h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.widget-modal-close {
background: none;
border: none;
font-size: 32px;
line-height: 1;
color: #666;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s ease;
}
.widget-modal-close:hover {
color: #000;
}
.widget-modal-body {
padding: 30px;
}
/* Responsive Design */
@media (max-width: 768px) {
.widget-modal-content {
width: 95%;
max-height: 95vh;
}
.widget-modal-header {
padding: 15px 20px;
}
.widget-modal-header h2 {
font-size: 20px;
}
.widget-modal-body {
padding: 20px;
}
}
Installation and Deployment
Installation Commands
# Enable the module
bin/magento module:enable Vendor_ModuleName
# Run setup upgrade
bin/magento setup:upgrade
# Compile dependency injection (production mode)
bin/magento setup:di:compile
# Deploy static content (production mode)
bin/magento setup:static-content:deploy -f
# Clear cache
bin/magento cache:flush
Deployment Checklist
- ✅ Module enabled
- ✅ Database schema updated (
setup:upgrade) - ✅ DI compiled (
setup:di:compile) - ✅ Static content deployed
- ✅ Cache cleared
- ✅ Permissions checked (www-data ownership)
Using the Widget
Method 1: Admin Panel (CMS Editor)
- Navigate to Content > Pages or Content > Blocks
- Edit the desired page or block
- Place cursor where widget should appear
- Click Insert Widget button in editor toolbar
- Select Widget Type: Your widget name
- Configure widget parameters
- Click Insert Widget
- Save the page/block
Method 2: Direct Code in CMS Content
Add widget code directly in CMS content:
{{widget type="Vendor\ModuleName\Block\Widget\WidgetName"
text_param="My Value"
select_param="value1"
enabled="1"}}
Method 3: XML Layout Files
Add widget programmatically in layout XML:
<referenceContainer name="content">
<block class="Vendor\ModuleName\Block\Widget\WidgetName"
name="custom.widget"
template="Vendor_ModuleName::widget/template.phtml">
<arguments>
<argument name="text_param" xsi:type="string">My Value</argument>
<argument name="select_param" xsi:type="string">value1</argument>
<argument name="enabled" xsi:type="boolean">true</argument>
</arguments>
</block>
</referenceContainer>
Method 4: Programmatically in Template
Create widget block in any template:
<?= $block->getLayout()
->createBlock(\Vendor\ModuleName\Block\Widget\WidgetName::class)
->setData('text_param', 'My Value')
->setData('enabled', true)
->toHtml() ?>
Real-World Example: Quote Request Form Widget
Based on the ItTools_QuoteForm module created for LCD Screen Repair:
Features:
- Modal popup form
- File upload (max 3 images, 5MB each)
- Client-side validation
- AJAX submission
- Email with attachments
- Success/error messages
Key Files:
app/code/ItTools/QuoteForm/
├── Block/Widget/QuoteButton.php
├── Controller/Index/Submit.php
├── etc/widget.xml
├── etc/email_templates.xml
├── view/frontend/templates/widget/quotebutton.phtml
├── view/frontend/web/js/quote-modal.js
├── view/frontend/web/js/quote-form.js
└── view/frontend/web/css/quote-form.css
Usage:
{{widget type="ItTools\QuoteForm\Block\Widget\QuoteButton"
button_text="Get a Free Quote"
button_class="action primary"}}
Common Widget Use Cases
- Contact Forms - Custom contact/inquiry forms
- Quote Request Buttons - Lead generation forms
- Product Sliders - Featured product carousels
- CTAs (Call-to-Action) - Promotional buttons/banners
- Newsletter Signup - Custom subscription forms
- Social Media Feeds - Display social content
- Store Locators - Find nearest store widget
- Calculators - Price/shipping calculators
- Reviews/Testimonials - Customer feedback display
- Search Boxes - Custom search functionality
Best Practices Summary
Security
- ✅ Always escape output in templates
- ✅ Validate and sanitize all user inputs
- ✅ Use form keys for POST requests
- ✅ Implement CSRF protection
- ✅ Check file types and sizes for uploads
- ✅ Use parameterized queries (avoid SQL injection)
- ✅ Validate email addresses properly
- ✅ Add rate limiting for form submissions
Performance
- ✅ Minimize database queries in blocks
- ✅ Use caching where appropriate
- ✅ Lazy-load JavaScript when possible
- ✅ Optimize CSS (remove unused styles)
- ✅ Compress images and assets
- ✅ Use CDN for static assets
- ✅ Avoid blocking JavaScript
Code Quality
- ✅ Use strict types (
declare(strict_types=1);) - ✅ Add type hints and return types
- ✅ Follow Magento coding standards
- ✅ Use dependency injection (no ObjectManager)
- ✅ Add proper PHPDoc comments
- ✅ Keep methods small and focused
- ✅ Use constants for magic values
- ✅ Implement proper error handling
- ✅ Log errors appropriately
- ✅ Write unit/integration tests
User Experience
- ✅ Make widgets responsive (mobile-friendly)
- ✅ Provide clear success/error messages
- ✅ Add loading indicators for AJAX
- ✅ Validate forms client-side and server-side
- ✅ Use accessibility attributes (aria-*)
- ✅ Test keyboard navigation
- ✅ Provide clear labels and instructions
- ✅ Handle edge cases gracefully
Maintainability
- ✅ Use meaningful variable/method names
- ✅ Keep templates clean (logic in blocks)
- ✅ Document complex logic
- ✅ Use configuration for settings
- ✅ Follow single responsibility principle
- ✅ Make code testable
- ✅ Version control properly
- ✅ Add README with usage instructions
Troubleshooting
Widget Not Appearing in Admin
Symptoms: Widget doesn't show in "Insert Widget" dropdown
Solutions:
- Check
widget.xmlsyntax (validate XML) - Verify module is enabled:
bin/magento module:status - Clear cache:
bin/magento cache:flush - Clear generated code:
rm -rf generated/code/* - Check file permissions
- Review
system.logfor errors
Widget Not Rendering on Frontend
Symptoms: Widget code shows but no output
Solutions:
- Verify template path in block class
- Check template file exists at specified path
- Clear cache:
bin/magento cache:flush - Check for PHP errors in template
- Review
exception.logandsystem.log - Enable developer mode to see detailed errors
JavaScript Not Loading
Symptoms: Widget functionality not working
Solutions:
- Verify
requirejs-config.jssyntax - Check JS file path is correct
- Clear static content:
bin/magento setup:static-content:deploy -f - Check browser console for 404 errors
- Verify file permissions
- Check for JavaScript errors in console
- Ensure jQuery and dependencies are loaded
CSS Styles Not Applied
Symptoms: Widget appears unstyled
Solutions:
- Verify CSS path in
layout/default.xml - Check CSS file exists
- Deploy static content:
bin/magento setup:static-content:deploy -f - Clear browser cache
- Check for CSS file 404 in network tab
- Verify CSS selector specificity
- Check for conflicting styles
Form Submission Failing
Symptoms: AJAX returns errors or no response
Solutions:
- Check controller route configuration
- Verify controller implements correct interface
- Add form key to form if using POST
- Check network tab for actual error response
- Review
exception.logfor server errors - Verify AJAX URL is correct
- Check request/response format (JSON)
- Ensure proper content type headers
File Upload Issues
Symptoms: Files not uploading or validation fails
Solutions:
- Check PHP
upload_max_filesizeandpost_max_size - Verify file input has
enctype="multipart/form-data" - Check file permissions on upload directory
- Validate file mime types server-side
- Check for JavaScript file validation logic
- Review file size limits (client and server)
- Check
$_FILESarray in controller
Modal Popup Issues
Symptoms: Modal is half-hidden, trapped inside section/div, has z-index issues, double overlays, or positioning problems
Root Causes:
- Conflict between Magento's
Magento_Ui/js/modal/modalcomponent and custom modal HTML structure - Modal element is constrained by parent container (overflow, position, z-index)
Solutions:
- CRITICAL: Move modal to body - Add
modalElement.appendTo('body')in widget initialization to escape parent containers - DO NOT use Magento's modal component when you already have custom modal HTML with overlay and content divs
- Use simple jQuery
fadeIn()/fadeOut()instead ofmodal('openModal') - Set very high z-index (999999) with !important on the modal container (themes often use 10000+ for headers/menus)
- Use
position: fixedon BOTH overlay and content (not absolute) - Use darker overlay background
rgba(0, 0, 0, 0.7)to clearly block content - Add
body.modal-open { overflow: hidden; }to prevent background scroll - Add
pointer-events: autoto modal and children to block clicks - Clear static content after changes:
rm -rf pub/static/frontend/* - Clear browser cache and test in incognito mode
- Inspect competing elements with browser DevTools to find their z-index values
Example Fix:
// WRONG - causes conflicts
define(['jquery', 'Magento_Ui/js/modal/modal'], function ($, modal) {
modalElement.modal({ ... });
});
// CORRECT - simple and works
define(['jquery'], function ($) {
modalElement.fadeIn(300);
$('body').addClass('modal-open');
});
Testing Checklist
Before deploying your widget:
Functional Testing
- Widget appears in admin "Insert Widget" dropdown
- All parameters show correctly in admin
- Widget renders on frontend
- All parameter variations work correctly
- Form submission works (if applicable)
- Validation works client-side and server-side
- Success/error messages display correctly
- Email sending works (if applicable)
Browser/Device Testing
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- Mobile iOS
- Mobile Android
- Tablet
Performance Testing
- Page load time acceptable
- No JavaScript errors in console
- No CSS conflicts with theme
- Caching works properly
- AJAX requests complete quickly
Security Testing
- All outputs escaped properly
- SQL injection prevention
- XSS prevention
- CSRF protection
- File upload validation
- Input validation/sanitization
Accessibility Testing
- Keyboard navigation works
- Screen reader friendly
- Proper ARIA labels
- Focus indicators visible
- Color contrast sufficient
Reference: ItTools Module Examples
Real working examples from this codebase:
ItTools_QuoteForm (
app/code/ItTools/QuoteForm/)- Modal popup widget
- Form with file uploads
- Email functionality
- AJAX submission
ItTools_CategorySearch (
app/code/ItTools/CategorySearch/)- Simple search widget
- Extends existing functionality
- Custom placeholder text parameter
Use these as reference implementations.
Additional Resources
Magento DevDocs
Code Examples
- Magento core widgets:
vendor/magento/module-widget/ - Magento CMS widgets:
vendor/magento/module-cms/Block/Widget/ - Catalog widgets:
vendor/magento/module-catalog/Block/Widget/
Version Compatibility
This skill is compatible with:
- Magento Open Source 2.3.x - 2.4.x
- Adobe Commerce 2.3.x - 2.4.x
- Mage-OS (Magento fork)
Not compatible with:
- Hyvä themes (use hyva-tailwind-integration skill instead)
- Magento 1.x