🎯

magento2-widget-creation

🎯Skill

from proxiblue/claude-skills

VibeIndex|
What it does

Develops custom, configurable Magento 2 widgets for CMS pages and blocks with comprehensive module structure and implementation guidance.

πŸ“¦

Part of

proxiblue/claude-skills(4 items)

magento2-widget-creation

Installation

πŸ“‹ No install commands found in docs. Showing default command. Check GitHub for actual instructions.
Quick InstallInstall with npx
npx skills add proxiblue/claude-skills --skill magento2-widget-creation
2Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

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.

Overview

# 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

xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">

```

Key Points:

  • setup_version is legacy but still commonly used
  • Add dependencies in - Magento_Cms and Magento_Widget are 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

xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">

Brief description of what the widget does

Description shown in admin

source_model="Magento\Config\Model\Config\Source\Yesno">

Select Block...

source_model="Magento\Catalog\Model\Category\Attribute\Source\Categories">

```

Widget Parameter Types:

  • text - Simple text input
  • select - Dropdown selection
  • multiselect - Multiple selections
  • block - CMS block picker
  • page - CMS page picker
  • conditions - Product/category conditions (advanced)

Important Attributes:

  • id - Unique identifier for the widget
  • class - Full namespaced path to your block class
  • required - Whether parameter is mandatory
  • visible - 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_');

?>

escapeHtml(__('Widget Title')) ?>

escapeHtml($paramValue) ?>

```

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 @var comments for IDE support

Common Escaping Methods:

```php

// Text content

escapeHtml($text) ?>

// HTML attributes

// JavaScript strings

data-value="escapeJs($value) ?>"

// URLs

// CSS

```

Step 6: Add JavaScript (Optional)

If your widget needs interactivity, add JavaScript using Magento's widget pattern.

File: view/frontend/requirejs-config.js

```javascript

/**

* Copyright Β© Vendor. All rights reserved.

*/

var config = {

map: {

'*': {

widgetName: 'Vendor_ModuleName/js/widget-script'

}

}

};

```

File: view/frontend/web/js/widget-script.js

```javascript

/**

* 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:

```php

```

Alternative Initialization with data-bind (Knockout.js):

```php

```

Step 7: Add CSS Styling (Optional)

File: view/frontend/layout/default.xml

Load your CSS globally across all pages.

```xml

xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">

```

File: view/frontend/web/css/widget-style.css

```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:

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

xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">

```

Route URL Pattern:

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:

Advanced: Modal Popup Widget

For complex interactions like modal forms:

Template with Modal:

```php

$modalId = 'modal-' . uniqid();

?>

```

Modal JavaScript:

IMPORTANT:

  1. Do NOT use Magento's Magento_Ui/js/modal/modal component when you have custom modal HTML structure - it creates conflicts with z-index, positioning, and double overlays
  2. ALWAYS move the modal element to on initialization to prevent parent container constraints (overflow, positioning, z-index)

```javascript

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:

```css

/**

* 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;

}

}

```

Best Practices Summary

Security

  1. βœ… Always escape output in templates
  2. βœ… Validate and sanitize all user inputs
  3. βœ… Use form keys for POST requests
  4. βœ… Implement CSRF protection
  5. βœ… Check file types and sizes for uploads
  6. βœ… Use parameterized queries (avoid SQL injection)
  7. βœ… Validate email addresses properly
  8. βœ… Add rate limiting for form submissions

Performance

  1. βœ… Minimize database queries in blocks
  2. βœ… Use caching where appropriate
  3. βœ… Lazy-load JavaScript when possible
  4. βœ… Optimize CSS (remove unused styles)
  5. βœ… Compress images and assets
  6. βœ… Use CDN for static assets
  7. βœ… Avoid blocking JavaScript

Code Quality

  1. βœ… Use strict types (declare(strict_types=1);)
  2. βœ… Add type hints and return types
  3. βœ… Follow Magento coding standards
  4. βœ… Use dependency injection (no ObjectManager)
  5. βœ… Add proper PHPDoc comments
  6. βœ… Keep methods small and focused
  7. βœ… Use constants for magic values
  8. βœ… Implement proper error handling
  9. βœ… Log errors appropriately
  10. βœ… Write unit/integration tests

User Experience

  1. βœ… Make widgets responsive (mobile-friendly)
  2. βœ… Provide clear success/error messages
  3. βœ… Add loading indicators for AJAX
  4. βœ… Validate forms client-side and server-side
  5. βœ… Use accessibility attributes (aria-*)
  6. βœ… Test keyboard navigation
  7. βœ… Provide clear labels and instructions
  8. βœ… Handle edge cases gracefully

Maintainability

  1. βœ… Use meaningful variable/method names
  2. βœ… Keep templates clean (logic in blocks)
  3. βœ… Document complex logic
  4. βœ… Use configuration for settings
  5. βœ… Follow single responsibility principle
  6. βœ… Make code testable
  7. βœ… Version control properly
  8. βœ… Add README with usage instructions

Troubleshooting

Widget Not Appearing in Admin

Symptoms: Widget doesn't show in "Insert Widget" dropdown

Solutions:

  1. Check widget.xml syntax (validate XML)
  2. Verify module is enabled: bin/magento module:status
  3. Clear cache: bin/magento cache:flush
  4. Clear generated code: rm -rf generated/code/*
  5. Check file permissions
  6. Review system.log for errors

Widget Not Rendering on Frontend

Symptoms: Widget code shows but no output

Solutions:

  1. Verify template path in block class
  2. Check template file exists at specified path
  3. Clear cache: bin/magento cache:flush
  4. Check for PHP errors in template
  5. Review exception.log and system.log
  6. Enable developer mode to see detailed errors

JavaScript Not Loading

Symptoms: Widget functionality not working

Solutions:

  1. Verify requirejs-config.js syntax
  2. Check JS file path is correct
  3. Clear static content: bin/magento setup:static-content:deploy -f
  4. Check browser console for 404 errors
  5. Verify file permissions
  6. Check for JavaScript errors in console
  7. Ensure jQuery and dependencies are loaded

CSS Styles Not Applied

Symptoms: Widget appears unstyled

Solutions:

  1. Verify CSS path in layout/default.xml
  2. Check CSS file exists
  3. Deploy static content: bin/magento setup:static-content:deploy -f
  4. Clear browser cache
  5. Check for CSS file 404 in network tab
  6. Verify CSS selector specificity
  7. Check for conflicting styles

Form Submission Failing

Symptoms: AJAX returns errors or no response

Solutions:

  1. Check controller route configuration
  2. Verify controller implements correct interface
  3. Add form key to form if using POST
  4. Check network tab for actual error response
  5. Review exception.log for server errors
  6. Verify AJAX URL is correct
  7. Check request/response format (JSON)
  8. Ensure proper content type headers

File Upload Issues

Symptoms: Files not uploading or validation fails

Solutions:

  1. Check PHP upload_max_filesize and post_max_size
  2. Verify file input has enctype="multipart/form-data"
  3. Check file permissions on upload directory
  4. Validate file mime types server-side
  5. Check for JavaScript file validation logic
  6. Review file size limits (client and server)
  7. Check $_FILES array 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:

  1. Conflict between Magento's Magento_Ui/js/modal/modal component and custom modal HTML structure
  2. Modal element is constrained by parent container (overflow, position, z-index)

Solutions:

  1. CRITICAL: Move modal to body - Add modalElement.appendTo('body') in widget initialization to escape parent containers
  2. DO NOT use Magento's modal component when you already have custom modal HTML with overlay and content divs
  3. Use simple jQuery fadeIn()/fadeOut() instead of modal('openModal')
  4. Set very high z-index (999999) with !important on the modal container (themes often use 10000+ for headers/menus)
  5. Use position: fixed on BOTH overlay and content (not absolute)
  6. Use darker overlay background rgba(0, 0, 0, 0.7) to clearly block content
  7. Add body.modal-open { overflow: hidden; } to prevent background scroll
  8. Add pointer-events: auto to modal and children to block clicks
  9. Clear static content after changes: rm -rf pub/static/frontend/*
  10. Clear browser cache and test in incognito mode
  11. Inspect competing elements with browser DevTools to find their z-index values

Example Fix:

```javascript

// 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');

});

```