How to Make a Field a WYSIWYG Editor in system.xml (Magento 2 Admin Config)
Magento has no type="wysiwyg" for system.xml. Here's the canonical frontend_model that turns a config field into a full TinyMCE editor, how to render the value safely per store view, and the gotchas that waste an afternoon.
Storing rich content in Magento’s Stores → Configuration is a clean way to let a client edit a promo banner, an email footer, or a shipping-policy block per store view without touching code or a CMS block. The catch: system.xml ships field types like text, textarea and select, but there is no wysiwyg type. This guide shows the canonical way to render a config field as a full TinyMCE editor on Magento 2.4.4-2.4.9, the way to read the value back safely, and the gotchas that send most people in circles.
The short answer
Point the field at a custom frontend_model that extends Magento\Config\Block\System\Config\Form\Field and switches the underlying form element into WYSIWYG mode. That is the whole trick, everything below is the detail and the edge cases.
Step 1, declare the field in system.xml
The field itself stays a normal field. The magic is the frontend_model node:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="panth_promo" translate="label" sortOrder="100"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Promo Content</label>
<tab>general</tab>
<resource>Panth_Promo::config</resource>
<group id="general" translate="label" sortOrder="10"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Promo Banner</label>
<field id="banner_html" translate="label comment" sortOrder="10"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Banner HTML</label>
<comment>Rich content shown above the header. Supports HTML and images.</comment>
<frontend_model>Panth\Promo\Block\Adminhtml\System\Config\Form\Field\Wysiwyg</frontend_model>
</field>
</group>
</section>
</system>
</config>
Note showInStore="1" on the section, group and field, that is what lets you scope the content to a single store view later.
Step 2, the frontend_model that becomes the editor
Create the block referenced above. It takes the plain textarea Magento would have rendered and promotes it to TinyMCE using the CMS WYSIWYG config:
<?php
declare(strict_types=1);
namespace Panth\Promo\Block\Adminhtml\System\Config\Form\Field;
use Magento\Backend\Block\Template\Context;
use Magento\Cms\Model\Wysiwyg\Config as WysiwygConfig;
use Magento\Config\Block\System\Config\Form\Field;
use Magento\Framework\Data\Form\Element\AbstractElement;
class Wysiwyg extends Field
{
public function __construct(
Context $context,
private readonly WysiwygConfig $wysiwygConfig,
array $data = []
) {
parent::__construct($context, $data);
}
protected function _getElementHtml(AbstractElement $element): string
{
// Promote the textarea into a TinyMCE instance.
$element->setWysiwyg(true);
// Trim the toolbar to what a config field actually needs.
$element->setConfig(
$this->wysiwygConfig->getConfig([
'add_variables' => false, // hide "Insert Variable"
'add_widgets' => false, // hide "Insert Widget"
'add_images' => true, // keep the media-gallery image button
'height' => '320px',
])
);
return parent::_getElementHtml($element);
}
}
That is it for the admin side. Reload Stores → Configuration → Promo Content and the field is a full editor. Because Magento\Cms\Model\Wysiwyg\Config supplies the adapter, you get the same TinyMCE that CMS pages use, no extra JS to register.
Step 3, read and render the value safely
The saved value lands in core_config_data as raw HTML. When you output it, run it through the CMS page filter (so {{media}} and any widget directives resolve) and print it without escaping, it is trusted, admin-authored HTML, and escaping it would show literal <p>/<br> tags:
<?php
declare(strict_types=1);
namespace Panth\Promo\Block;
use Magento\Cms\Model\Template\FilterProvider;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Store\Model\ScopeInterface;
class Banner extends Template
{
public function __construct(
Context $context,
private readonly ScopeConfigInterface $scopeConfig,
private readonly FilterProvider $filterProvider,
array $data = []
) {
parent::__construct($context, $data);
}
public function getBannerHtml(): string
{
$raw = (string) $this->scopeConfig->getValue(
'panth_promo/general/banner_html',
ScopeInterface::SCOPE_STORE
);
return $raw === '' ? '' : $this->filterProvider->getPageFilter()->filter($raw);
}
}
And in the template:
<?= /* @noEscape */ $block->getBannerHtml() ?>
The gotchas that waste an afternoon
The field renders as a plain textarea
Nine times out of ten this is not your code. Check, in order: (1) flush the cache, bin/magento cache:flush; (2) confirm Magento_Cms is enabled (the WYSIWYG adapter lives there); (3) in production mode, redeploy admin assets with bin/magento setup:static-content:deploy -f; (4) make sure no theme or extension globally disabled the WYSIWYG editor under Content → Configuration.
The image button throws an error
The media gallery expects a store context, which the default config scope does not have. If you see an error opening the image dialog, set 'add_images' => false for the config field, or only enable the editor at website/store scope.
My HTML disappears on save
System configuration does not strip HTML by default. If your value is being mangled, you almost certainly added a backend_model that sanitizes it, remove it, or make it pass the HTML through untouched. The raw markup belongs in core_config_data as-is.
Same content on every store view
You forgot showInStore="1", or you are reading the value with the wrong scope. Read with ScopeInterface::SCOPE_STORE and the current store is resolved automatically.
When a textarea is the better call
If the admin only needs line breaks and the odd link, not a full editor, skip the WYSIWYG entirely and use <field ... type="textarea">. It is one line, has no JS dependency, and avoids the media-gallery edge cases. Reach for the editor only when non-technical users genuinely need formatting controls.
Wrap-up
A WYSIWYG field in system.xml is three small pieces: the field with a frontend_model, the block that flips it into TinyMCE, and a frontend block that filters and prints the value unescaped. Keep the toolbar minimal, scope it per store, and remember that a “broken” editor is nearly always a cache or asset-deploy issue rather than a code one.
Need a custom admin configuration, a bespoke extension, or a hand wiring this into a real store? That is exactly what my Magento extension development service covers, and if you are modernising the storefront too, see Hyvä theme development.