Visit an article's edit page and you'll see tabs across the top — "View," "Edit," "Delete," maybe "Revisions." Those aren't menu links buried in a sidebar; they're local tasks, a different navigation mechanism purpose-built for "here are the other things you can do with the exact thing you're already looking at." This lesson covers .links.task.yml, the file that defines them.
What you'll learn in this lesson
- How local tasks differ from ordinary menu links, and when to reach for each
base_route— the key that groups a set of tabs together- How to build two levels of tabs (primary and secondary) on the same page
- Why one tab in a group gets a negative weight by default
The source file
Path (relative to the Examples module's root): modules/menu_example/menu_example.links.task.yml
# This file creates static local tasks (Tabs).
# This file will be needed to place in module root.
#
# 'title' of the tab will show up on the user interface and tab.
# 'base_route' is the same as the name of the route where the "default" tab
# appears. The base_route is used to group together related tabs.
# 'weight' is used to provide weights for the tabs if needed.
# The tab whose route is the same as the base_route will by default
# get a negative weight and appear on the left.
# 'parent_id' is used to create multi level of tabs.
# To relate a tab to its parent use same name as parent_id as shown below in
# examples.menu_example.tabs.secondary.
#
examples.menu_example.tabs:
route_name: examples.menu_example.tabs
title: Default primary tab
base_route: examples.menu_example.tabs
examples.menu_example.tabs_second:
route_name: examples.menu_example.tabs_second
title: Second
base_route: examples.menu_example.tabs
weight: 2
examples.menu_example.tabs.secondary:
route_name: examples.menu_example.tabs
title: Default secondary tab
parent_id: examples.menu_example.tabs
examples.menu_example.tabs_default_second:
route_name: examples.menu_example.tabs_default_second
title: Second
parent_id: examples.menu_example.tabs
How it works, key by key
base_route — the key that ties tabs together into one group
Every local task that should appear as a tab on the same page shares the same base_route value. It doesn't have to be the route this specific tab links to — examples.menu_example.tabs_second has its own distinct route_name, but shares the group's base_route so it renders as a sibling tab alongside the default one.
The "default" tab gets a negative weight automatically
Notice examples.menu_example.tabs has a route_name that's identical to its own base_route. Drupal recognizes this pattern and treats it as the group's default tab, automatically pulling it to the leftmost position — you don't need to set weight on it yourself for that positioning to happen, though you still can to fine-tune order among the rest.
weight among non-default tabs
examples.menu_example.tabs_second:
route_name: examples.menu_example.tabs_second
title: Second
base_route: examples.menu_example.tabs
weight: 2
Same ordering rule as ordinary menu links: lower weight sorts first. Since the default tab already claims the leftmost spot implicitly, weight here really just controls order among everything after it.
parent_id — a second row of tabs
examples.menu_example.tabs.secondary:
route_name: examples.menu_example.tabs
title: Default secondary tab
parent_id: examples.menu_example.tabs
This is the mechanism behind pages that show two rows of tabs — think a node's primary tabs (View/Edit/Delete) with a secondary row underneath for something like translations. Setting parent_id to another local task's own machine name nests it as a secondary tab beneath that primary one, using the exact same "route matching parent equals default" trick to decide which secondary tab is selected by default.
See it for yourself
Visit /examples/menu-example/tabs on your own DDEV site. You'll see a row of primary tabs (Default primary tab / Second / Third / Fourth) and, underneath, a second row of secondary tabs — both generated entirely from this one YAML file, no controller logic involved in deciding which tabs to show.
Quick check: you want to add a third row of tabs nested even deeper than secondary. Does
.links.task.ymlsupport that? No — Drupal's local task system only supports two levels, primary and secondary. A third level needs a different UI pattern entirely (commonly a set of links rendered inside the page content itself), not a deeperparent_idchain.
Key takeaways
- Local tasks (tabs) answer "what else can I do with this specific page," while menu links answer "where can I go from anywhere" — choose based on that distinction, not habit.
base_routegroups tabs together; the tab whose ownroute_namematches the group'sbase_routebecomes the default, leftmost tab automatically.parent_idcreates a second, secondary row of tabs nested under a specific primary tab — Drupal core only supports these two levels.weightorders tabs within their group exactly like it orders sibling menu links — lower sorts first.
Coming up next
Tabs cover "what else can I do here" for viewing content. The next lesson covers a third navigation mechanism aimed specifically at actions — the "Add contact," "Add content" style buttons that show up above a list, defined in .links.action.yml.