Chat on WhatsApp
Magento Development 8 min read

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.

How to Make a Field a WYSIWYG Editor in system.xml (Magento 2 Admin Config)

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.