Skip to content

How-to guideFor module developersDrupal 11

Menus and links

Menu links, the tabs on a page, and the action buttons above it are all plugins your module declares in YAML files. This guide adds each kind, then generates a family of links at runtime with a deriver.

Place a module’s pages into Drupal’s menu system: a link in the administration menu, tabs to switch between related pages, an action button to start a task, and one dynamic link per content type. If you only need an editor-managed link, add it in the UI at Administer → Structure → Menus instead; this guide is for links your module ships.

Menu links live in MODULE.links.menu.yml. Each top-level key is the link’s plugin ID; route_name is where it points and parent is the link it nests under.

web/modules/custom/howto_menus/howto_menus.links.menu.yml
# A static menu link. The key is the plugin ID; one entry, one link.
howto_menus.overview:
title: 'Menus how-to'
description: 'Example page for the menus and links guide.'
route_name: howto_menus.overview
parent: system.admin_reports
weight: 50

parent names another menu link plugin ID (system.admin_reports is the Reports section), and the link joins its parent’s menu. Without a parent, the link lands at the top level of the Tools menu unless you set menu_name (for example admin). Rebuild caches after editing the file: drush cr.

Tabs are local tasks, declared in MODULE.links.task.yml. The base_route ties a group of tabs together; the task whose route_name equals its base_route is the default (leftmost) tab.

web/modules/custom/howto_menus/howto_menus.links.task.yml
# Local tasks render as the tabs at the top of a page. The default task
# must point at the same route as the page it tabs; siblings add more tabs.
howto_menus.overview:
title: 'Overview'
route_name: howto_menus.overview
base_route: howto_menus.overview
howto_menus.settings:
title: 'Settings'
route_name: howto_menus.settings
base_route: howto_menus.overview
weight: 10

Two or more tasks sharing a base_route render as tabs on every page in that group. A single task renders nothing, so tabs always come in sets.

Action buttons, the “Add” buttons above a listing, are local actions in MODULE.links.action.yml. appears_on lists the routes the button shows on.

web/modules/custom/howto_menus/howto_menus.links.action.yml
# Local actions render as the action buttons (e.g. "Add") above a page.
# appears_on lists the routes the button shows up on.
howto_menus.add_article:
title: 'Add article'
route_name: node.add
route_parameters:
node_type: article
appears_on:
- howto_menus.overview

When the set of links is data-driven, one link per content type, say, a deriver turns one base definition into many. Name the deriver class in the base link:

web/modules/custom/howto_menus/howto_menus.links.menu.yml
# A derivative-based link: one base entry, many links at runtime. The
# deriver returns one definition per content type, merged onto this base.
howto_menus.add:
title: 'Add content'
parent: system.admin_content
deriver: \Drupal\howto_menus\Plugin\Derivative\ContentTypeAddLinks

The deriver returns one definition per variant, merged onto the base. The plugin IDs become BASE:DERIVATIVE (here howto_menus.add:article).

web/modules/custom/howto_menus/src/Plugin/Derivative/ContentTypeAddLinks.php
<?php
declare(strict_types=1);
namespace Drupal\howto_menus\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Derives one menu link per content type.
*
* A deriver turns one base plugin definition into many at runtime. The base
* definition in *.links.menu.yml names this class, and
* getDerivativeDefinitions() returns one definition per variant.
*/
final class ContentTypeAddLinks extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* Constructs a ContentTypeAddLinks deriver.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id): static {
return new static(
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition): array {
$node_types = $this->entityTypeManager->getStorage('node_type')->loadMultiple();
foreach ($node_types as $id => $node_type) {
$this->derivatives[$id] = [
'title' => $this->t('Add @label', ['@label' => $node_type->label()]),
'route_name' => 'node.add',
'route_parameters' => ['node_type' => $id],
] + $base_plugin_definition;
}
return $this->derivatives;
}
}
  • Rebuild caches and visit the page: drush cr, then /admin/reports/howto-menus. The tabs and the action button appear, and the link shows under Reports in the administration menu.

  • Confirm the deriver from the command line:

    Terminal window
    drush php:eval "var_dump(\Drupal::service('plugin.manager.menu.link')->getDefinition('howto_menus.add:article') !== NULL);"

    prints bool(true) once at least one content type exists.

For links site builders should control, use content menu links (the menu_link_content entity) created in the UI or with the configuration management workflow, not the plugin links above.

Contextual links (MODULE.links.contextual.yml) and menu link parameters follow the same YAML shape. See the menu API topic for the full set.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.