Mugo Web main content.

Limited RichText formatting per field in the Ibexa DXP admin UI

By: Thiago Campos Viana | July 22, 2026 | ibexadxp and ez platform

When creating content in Ibexa using the RichText Editor, it is easy to realize that not every RichText field needs the same toolbar. A ”Body” field may need headings, embeds, and tables. A ”Short Description” or ”Caption” field usually needs bold, italic, and little else. When every field gets the full CKEditor toolbar, it might make editors waste time or even make them think they can add markup your templates were never built for.

Legacy eZ Publish solved this. Ibexa DXP does not, at least out of the box. In this blog post we describe how we added this feature to Ibexa 4.

What legacy eZOE provided

On eZ Publish sites using eZOE, toolbar buttons could be limited per field. Layouts were defined in ezoe.ini (often via an append file), for example:

[EditorLayout_mini]
Buttons[]
Buttons[]=bold
Buttons[]=italic
Buttons[]=underline
Buttons[]=sub
Buttons[]=sup
Buttons[]=|
Buttons[]=undo
Buttons[]=redo

When editing a content class, you could assign a tag preset to each class attribute. At edit time, eZOE resolved it to an [EditorLayout_*] section.

Result: “Article/Short Description” could show a minimal toolbar while “Article/Body” kept the full editor on the same form.

What Ibexa DXP provides instead

Ibexa DXP uses CKEditor 5. Toolbar buttons are configured globally per siteaccess in YAML (ibexa.fieldtypes. ezrichtext.toolbar). One toolbar config applies to every rich text field.

There is no per-field toolbar setting in the content type UI. There is no equivalent of a tag preset resolving through ezoe.ini.

For migrations from eZ Publish, this is a common regression. Sites that relied on a custom layout, “mini” or similar presets lose field-specific editing constraints unless they rebuild them.

The solution

This approach limits toolbar buttons only. It does not block copy-and-paste of disallowed markup, and it does not strip formatting from existing content on load. Stricter enforcement is out of the scope. For most cases, toolbar restriction is enough: it guides editors without turning the field into a validation pipeline.

Ibexa DXP supports per-field toolbar customization, but you have to implement the logic yourself.

First, we define our custom layouts and field mappings in YAML. Here is an example configuration translating the eZOE layout above:

layouts:
 mini:
   buttons:
     - bold
     - italic
     - underline
     - subscript
     - superscript
     - '|'
     - undo
     - redo

data_types:
 - { key: 'article__short_description', layout: 'mini' }

Named layouts are whitelists of CKEditor toolbar items. Field mappings tie a content type and field identifier to a layout. Unmapped fields keep the full default toolbar.

Field keys use <content_type>__<field_identifier>. The all__field form applies a layout to that field name on any content type.

Next, we need to bridge this backend configuration with our frontend editor. We do this in two steps: exposing the data via a Twig component, and loading our script via Webpack.

1. Exposing the configuration

To make our YAML configurations available to JavaScript we leverage Ibexa DXP’s Admin UI component system. By registering a custom Twig template as a service and tagging it with the content-edit-form-after group, Ibexa DXP automatically injects our template at the bottom of the edit forms. We use this template purely to output our configuration data for JavaScript to read.

Example:

{% block javascripts %}    {% set layouts = get_mugo_rich_text_custom_layout_config('layouts') %}    {% set data_types = get_mugo_rich_text_custom_layout_config('data_types') %}    <script>        window.mugorichtextcustomlayout_layouts = {{ layouts|json_encode|raw }};        window.mugorichtextcustomlayout_data_types = {{ data_types|json_encode|raw }};        window.mugorichtextcustomlayout_current_class_identifier = '{{ content.contentType.identifier }}';    </script> {%- endblock -%}

The Twig function to get the configuration can be defined like this:

<?php namespace MugoWeb\RichTextCustomLayoutBundle\Twig; use Symfony\Component\DependencyInjection\Container; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; class Extension extends AbstractExtension {    /** @var Container */    protected $container;    public function __construct(Container $container)    {        $this->container = $container;    }    public function getName()    {        return 'mugo_rich_text_custom_layout_settings';    }    public function getFunctions()    {        return array(            new TwigFunction('get_mugo_rich_text_custom_layout_config', array($this, 'getConfig'))        );    }    public function getConfig( $key )    {        $namespace = 'mugo_rich_text_custom_layout_settings';        $configResolver = $this->container->get( 'ibexa.config.resolver' );        if ( $configResolver->hasParameter( $key, $namespace ) )        {            return $configResolver->getParameter( $key, $namespace );        }        $defaultParameter = "{$namespace}.default.{$key}";        if ( $this->container->hasParameter( $defaultParameter ) )        {            return $this->container->getParameter( $defaultParameter );        }        return null;    } }

2. Loading the JavaScript listener

To load our actual JavaScript file, we avoid creating a new Encore entry. Instead, we append our script to the existing ibexa-richtext-onlineeditor-js Webpack entry from our Symfony bundle:

const path = require('path'); module.exports = (ibexaConfig, ibexaConfigManager) => {    ibexaConfigManager.add({        ibexaConfig,        entryName: 'ibexa-richtext-onlineeditor-js',        newItems: [path.resolve(__dirname, '../public/js/fieldtype.richtext.toolbar-modifier.js')],    }); };

3. Filtering the toolbar

Inside that custom script (fieldtype.richtext.toolbar-modifier.js), we filter the toolbar based on the specific field. The recommended hook is the ibexa-ckeditor:configure event found in base-ckeditor.js, which fires right before each editor instance is created:

const customEvent = new CustomEvent('ibexa-ckeditor:configure', {    detail: { container, config }, }); doc.body.dispatchEvent(customEvent); createCKEditor();

Listen for that event, identify which field is initializing from container, and modify config.toolbar.items before the editor renders.

There are some small details that might require attention, for example, eZOE's separate unlink button has no CKEditor toolbar equivalent; unlinking is handled inside Ibexa DXP's link UI.

Why this approach?

Global toolbar reduction affects every field, including body copy that needs the full editor.

Separate field types multiply content model complexity for what is essentially a UI concern.

Forking the rich text bundle works but carries upgrade cost.

A new Encore entry plus template override works but is heavier than appending to ibexa-richtext-onlineeditor-js, which Ibexa already supports.

The ibexa-ckeditor:configure hook is small, upgrade-safe, and matches Ibexa's own guidance. YAML layouts and field mappings sit on top without needing any kernel hack.

Conclusion

Per-field RichText toolbars were a standard eZ Publish pattern through eZOE presets and ezoe.ini layouts. Ibexa DXP exposes a global toolbar and a configure event. Rebuilding the old behavior means listening to that event, filtering toolbar items per field, and driving the rules from configuration rather than hard-coded checks.

If you are migrating from eZ Publish, tackle this early. Constraining the toolbar at the source is much simpler than cleaning up unexpected markup in your templates later on. Plus, providing a tailored, clutter-free toolbar vastly improves the content editor's writing workflow.

Thanks to Ibexa for pointing us to the ibexa-ckeditor:configure event and sharing a minimal proof of concept that removed a single toolbar button per field. That starting point made the full, configuration-driven solution straightforward to build on top of.