Change record status: 
Introduced in branch: 

This change record has been updated based on

As a part of the new New Symfony-based routing system, the local task (tab) definitions are moving out of hook_menu() and into plugins implementing \DrupalCore\Menu\LocalTaskInterface and annotated with \Drupal\Core\Annotation\Menu\LocalTask which are discovered using \Drupal\Core\Plugin\Discovery\YamlDiscovery which looks for YAML files in the same directory as your module with the file name pattern MODULENAME.links.task.yml. For example, views_ui has views_ui.links.task.yml. The base implementation \Drupal\Core\Menu\LocalTaskBase is used as the default class and will be suitable for the majority of standard uses. It can be extended for more advanced uses, such as rendering a dynamic title.

Drupal 7

function user_menu() {
  $items['user/autocomplete'] = array(
    'title' => 'User autocomplete',
    'page callback' => 'user_autocomplete',
    'access callback' => 'user_access',
    'access arguments' => array('access user profiles'),
    'type' => MENU_CALLBACK,
    'file' => '',

  // Registration and login pages.
  $items['user'] = array(
    'title' => 'User account',
    'title callback' => 'user_menu_title',
    'page callback' => 'user_page',
    'access callback' => TRUE,
    'file' => '',
    'weight' => -10,
    'menu_name' => 'user-menu',

  $items['user/login'] = array(
    'title' => 'Log in',
    'access callback' => 'user_is_anonymous',

  $items['user/register'] = array(
    'title' => 'Create new account',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('user_register_form'),
    'access callback' => 'user_register_access',
    'type' => MENU_LOCAL_TASK,

  $items['user/password'] = array(
    'title' => 'Request new password',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('user_pass'),
    'access callback' => TRUE,
    'type' => MENU_LOCAL_TASK,
    'file' => '',


return $items;

Drupal 8

We still need hook_menu entries for the breadcrumb - note that this is a temporary backward-compatibility shim.

These example tabs are simply using the base class - and all the needed definition information is encapsulated in the YAML:

  title: 'Log in'
  route_name: user.pass
  title: 'Request new password'
  route_name: user.register
  title: 'Create new account'
  base_route :
  weight: 10

Each local task is represented by a plugin. Each plugin definition (found in the $module.links.task.yml file) contains three to five local-task specific keys underneath the top-level key which is the unique plugin ID. Plugin ID is an arbitrary string, but a recommended pattern for the plugin ID is: the route name maybe with .task or .tab when it needs to be clear that is just a string (not exactly the route name).

Keys are:

  • route_name: The machine name of the local task route - this also determines where it's displayed.
  • title: The title of the local action. By default, it will be passed through t() and localized. Strings with spaces should use single quotes.
  • title_context (optional): context for t()
  • base_route: The route where the "root" tab (generally the top, leftmost one) is displayed and which serves to group a set of tabs.
  • parent_id (optional): The plugin ID of the tab that is the parent - only relevant for 2nd level tabs. If this is set, base_route should be omitted and will be supplied from the parent
  • weight: (optional) The integer weight (lower weight tabs are further left, default is 0).

You cannot specify local tasks in the same file as the routes - they re defined independently.

If your local tasks link to a route that has variables in the route path (e.g. /node/{node}) the variable values will be provided from the values on the current request. E.g. if /node/3 is is the path in the URL, the View and Edit tabs will (by default) have '3' substituted for {node}. Note that you have full control over this behavior by implementing your own plugin class for a specific tab.

These plugins can also have a different class. Provide the class key for this in the local task YAML file with the name of the appropriate class as value.

Local tasks can also have derivatives (dynamically generated local tasks). To implement those, specify the deriver key with the name of an appropriate class, eg:

  deriver: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks'
  weight: 100

Then make that class extend DerivativeBase and implement ContainerDerivativeInterface. You can take additional argument via a static create() and return derivatives from the getDerivativeDefinitions() method which should override the parent implementation.

The default class for local tasks is \Drupal\Core\Menu\LocalTaskDefault

Note that the confusing concept/type MENU_DEFAULT_LOCAL_TASK is now gone - the "root" tab simply lists it's own route as the base_route, and a second level tab can link to the same route as its parent.

In order for a local task to be converted, both it and the path it appears on must first be converted to routes.
See #1971384: [META] Convert page callbacks to controllers for the list of issues and the WSCCI Conversion guide for instructions.

Because the MENU_LOCAL_TASK type did more than just present it as a local task (tab), it also manipulated the breadcrumb trail. In order to preserve that behavior each tab should have an entry of type MENU_VISIBLE_IN_BREADCRUMB left in hook_menu, at least until the breadcrumb code is fixed to remove this legacy behavior. Breadcrumbs are now defined by default by a path-based breadcrumb builder and thus is independent of local task definitions or menu links. for details see

The reference documentation page for local tasks plugins is

(Updated example route names for Convert all routes to 'module_name.foo_bar' naming convention )
(Updated YAML examples for #2107531: Improve DX of local task YAML definitions)

Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Module developer documentation done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other updates done

"Providing module-defined local tasks" is a doc within the Module developer documentation "Working with the Drupal API" section.


jessebeach’s picture

LocalTasks for routes that contain parameters are not yet working. See #2045267: LocalTaskBase and LocalTaskInterface has to work with routes containing parameters

pwolanin’s picture

martin107’s picture

Please forgive a minor nit pick to an otherwise clear and useful article.

The quoted files




vacho’s picture

This is quite confusing.
Why not defined once the menus entirely on the .routing.yaml?

by what I see. we are defining the path and other functions in the .routing.yaml
but menu item and we still use the hook_menu. Why?


Tshwarelo’s picture

I want 'route_name' to point to a link created by the Views module. Doing the following creates a fatal error:
    route_name : '/monitoring/critical-sites'
    title: 'Critical Sites List'
Tshwarelo’s picture

I ended up digging in the database. So route_name shows on the 'menu_tree' table.