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.