Custom Section in the Backend Navigation
Access to MetaModel data entry is often best placed in a dedicated section in the backend navigation. To do this, a corresponding group must be created, to which the desired model(s) can be assigned in the input mask properties under “Backend section”.

Via Configuration (from MM 2.5, recommended)
Since MetaModels 2.5, such a group can be created directly via config.yaml — with no
custom code required:
meta_models_core:
be_sections:
products:
name:
de: 'Produkte'
en: 'Products'
tooltip:
de: 'Produkte erstellen'
en: 'Create products'
icon: 'files/theme/mm/products.svg'
add:
before: design
products is the unique alias of the section — it is later selected under “Backend
section” in the input mask properties.
name(required) — language map for the label. The current backend language is displayed, falling back to English, then to the first available entry.tooltip(optional) — language map for the tooltip, resolved the same way asname. If not specified,nameis used.icon(optional) — web path to an icon, typically located under the Contao file managerfiles/…. If not specified — or if the specified file cannot be found — the grey MetaModels default icon is displayed.add(required) — sets the position relative to an existing navigation entry, using exactly one ofbeforeorafter.collapsed(optional, defaultfalse) — makes the section start collapsed on first load.
Note
The target alias under add is the internal Contao group name, not the
displayed label — the “Layout” section has internally been called design since
Contao 4/5, not layout. Common targets are content, design, accounts,
system, or the alias of another section that was itself created via
configuration. If the specified alias cannot be found in the navigation, the custom
section is appended to the end instead.
This configuration only creates the empty group. It is populated as usual: enter the
chosen alias (here products) under “Backend section” in the input mask properties of
a MetaModel.
See also
core#1519, as well as the section “Custom Backend Sections via Configuration” in Changes and Features in MM 2.5.
Manually via Event Listener (for Special Cases)
If the configuration above is not sufficient — for example because the visibility or
label of the section needs to depend on runtime conditions (logged-in user, database
content, etc.) — the same section can still be built via a custom MenuEvent
listener, as it was the only option before MM 2.5.
This requires an SVG icon and an assignment via the Contao MenuEvent.
SVG icons can be downloaded from e.g. material.io — the width, height, and fill colour should be adjusted as shown in the example using a text editor:
1 <svg xmlns="http://www.w3.org/2000/svg" fill="#91979c" width="15" height="15" viewBox="0 0 24 24">
2 <path d="...."/>
3 </svg>
The file can be saved e.g. at files/backend/group_icon_mm-test.svg (make the folder public).
An event listener is also required to create the entry — the group can be configured via the following parameters (lines 30 to 34):
$nodeName — alias of the entry
$nodeTitle — title
$nodeIcon — path to the icon
$targetNode — search for an existing entry such as “content” for content items
$targetType — whether the entry should appear
beforeorafterthe “targetNode”
Place the listener at src/EventListener/BackendMenuListener.php and run composer install.
1<?php
2
3namespace App\EventListener;
4
5use Contao\CoreBundle\Event\MenuEvent;
6use Knp\Menu\Util\MenuManipulator;
7use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
8use Symfony\Component\HttpFoundation\RequestStack;
9use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
10
11#[AsEventListener(priority: -1)]
12class BackendMenuListener
13{
14 private array $targetTypes = ['before' => 0, 'after' => 1];
15
16 public function __construct(
17 private readonly RequestStack $requestStack,
18 ) {
19 }
20
21 public function __invoke(MenuEvent $event): void
22 {
23 $factory = $event->getFactory();
24 $tree = $event->getTree();
25
26 if ('mainMenu' !== $tree->getName()) {
27 return;
28 }
29
30 $nodeName = 'mm-test';
31 $nodeTitle = 'My MM Category';
32 $nodeIcon = '/files/backend/group_icon_mm-test.svg';
33 $targetNode = 'content';
34 $targetType = 'after';
35
36 $categoryNode = $tree->getChild($nodeName);
37 if (!$categoryNode) {
38 $sessionBag = $this->requestStack->getSession()->getBag('contao_backend');
39 $status = ($sessionBag instanceof AttributeBagInterface) ? $sessionBag->get('backend_modules') : [];
40 $isCollapsed = ($status[$nodeName] ?? 1) < 1;
41
42 $categoryNode = $factory
43 ->createItem($nodeName)
44 ->setLabel($nodeTitle)
45 ->setUri('/contao?mtg=' . $nodeName)
46 ->setLinkAttribute('class', 'group-' . $nodeName)
47 ->setLinkAttribute('title', $nodeTitle)
48 ->setLinkAttribute('data-action', 'contao--toggle-navigation#toggle:prevent')
49 ->setLinkAttribute('data-contao--toggle-navigation-category-param', $nodeName)
50 ->setLinkAttribute('aria-controls', $nodeName)
51 ->setLinkAttribute('aria-expanded', $isCollapsed ? 'false' : 'true')
52 ->setChildrenAttribute('id', $nodeName)
53 ->setLinkAttribute('style', \sprintf('background: url(%s) 3px 2px no-repeat;', $nodeIcon))
54 ->setExtra('translation_domain', false);
55
56 if ($isCollapsed) {
57 $categoryNode->setAttribute('class', 'collapsed');
58 }
59
60 $tree->addChild($categoryNode);
61
62 $targetPosition = \array_search($targetNode, \array_keys($tree->getChildren()), true);
63 $targetPosition = false === $targetPosition ? 0 : $targetPosition + $this->targetTypes[$targetType];
64 $manipulator = new MenuManipulator();
65 $manipulator->moveToPosition($categoryNode, $targetPosition);
66 }
67 }
68}
69?>
The listener and a dummy SVG are available here for download.