---
title: Add and configure menu items
slug: cms-11/dev-docs/adding-and-configuring-menu-items
docTags: 
createdAt: 2026-07-22T19:45:21.284Z
---

## MenuItem attribute

Use `MenuItem` on MVC controllers. `MenuItem` adds links to the top menu (and requires the module to be registered; see [Configuring Shell modules](docId\:DNal_JrQ2rB1HrWlcgbGo). The required `menuPath` parameter is a logical path of the menu element in the menu. Optionally, you can add the `Url` parameter to specify the linked URL to the interface. By default, the URL is set to the registered shell route for the module (`/EPiServer/{ShellModule}/{controller}/{action}` for protected modules and `/modules/{ShellModule}/{controller}/{action}` for public modules).

The following example adds a menu item to the top menu bar with the text **Start** and infers the URL from the registered route for the shell module:

```csharp
public class DashboardController: Controller {
  [MenuItem("/global/dashboard", Text = "Start")]
  public ActionResult Index() {}
}
```

When you add menu items, you need to implement the page to include Optimizely Content Management System (CMS) styling for the shell and render the menu so you can return.

The key function to render a menu is `@Html.CreatePlatformNavigationMenu()`.

Note that the calls to `Html.ShellInitializationScript()` might still be needed depending on what content you are showing in addition to the navigation menu. It is not necessary for the navigation menu itself.

In addition to `Html.CreatePlatformNavigationMenu`, you also need to wrap content below the menu in an element decorated by using this HTML helper `html.ApplyPlatformNavigation()`.

:::hint{type="info"}
**Note**
The Platform Navigation font sizing uses REM and expects the root element to have size 16px (browser default). Some style frameworks, such as Bootstrap 3, override the root font size while others, like Bootstrap 4, do not.
:::

To adapt the example above to work with versions 11.21 and higher, you need to start using `CreatePlatformNavigationMenu()` and `ApplyPlatformNavigation()`. Note that you no longer need `@Html.Raw(ClientResources.RenderResources("Navigation"))` nor `@Html.Raw(Html.ShellInitializationScript())`:

```csharp
@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Navigation
    
<!DOCTYPE html>
<html>
  <head>
    <title>@ViewBag.Title</title>
    
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    
    <!-- Shell -->
    @ClientResources.RenderResources("ShellCore")
    
    <!-- LightTheme -->
    @ClientResources.RenderResources("ShellCoreLightTheme")
    
  </head>
  <body>
    @Html.CreatePlatformNavigationMenu()
    <div @Html.ApplyPlatformNavigation()>
      @RenderBody()
    </div>
  </body>
</html>
```

### Localize menu items with the MenuItem attribute

You can localize menu items by specifying `TextResourceKey`. By default, is the key resolved against `LocalizationService`. Alternatively attribute `ResourceType` can be specified that they should be a class a with a static property named `TextResourceKey` that returns the localized text.

Example:

```javascript
[MenuItem("/global/dashboard", TextResourceKey = "Start", ResourceType = typeof(MenuResources))]
```

### Organize menu items

The menu path organizes menu items in a tree structure. Menu items in the top menu are in the `/global` bucket. The next segment is the name of the menu section, for example, `/global/cms`. The last segment represents the actual user interface, for example, `/global/cms/edit`.

### Permissions with the MenuItem attribute

By default, CMS applies this security policy to shell module controllers. You can configure the policy through attribute `authorizationPolicy` in [module.config](docId\:dI64jrVOXLd4rWo8gsedO). You can then evaluate the availability of the menu item through the policy. You can apply the [AuthorizeAttribute](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute?view=aspnetcore-5.0) to specify a custom authorization to complement the default policy that applies to the module. Note, however, that if there is a policy for the shell module, it also evaluates that policy for the request.

```csharp
public class DashboardController: Controller {
  [MenuItem("/global/dashboard")]
  [Authorize(Roles = "SomeGroup")]
  public ActionResult Index() {}
}
```

## Examples

### Global menu button

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/j2_OQYDagxE8yEOQiY2dw_579e60d-examples-01-global-button-2.png)

```csharp
namespace Alloy.Business {
  public class EpiServerWorldGlobalMenuItem: Controller {
    [MenuItem(MenuPaths.Global + "/globalLink", SortIndex = SortIndex.First - 10, Text = "Optimizely world",
      Url = "https://world.episerver.com/cms/")]
    public ActionResult Index(string viewName) {
      return View();
    }
  }
}
```

### Extend the CMS menu section.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/cAkX00NsMbm8lK57GWXRp-20260722-194638.png "examples-02-extending-cms-section-2.png")

```csharp
namespace Alloy.Business {
  [MenuProvider]
  public class CmsMenuProvider: IMenuProvider {
    private readonly ModuleTable _modules;

    public CmsMenuProvider(ModuleTable modules) {
      _modules = modules;
    }

    public IEnumerable<MenuItem> GetMenuItems() {
      var menuItems = new List<MenuItem>();
      menuItems.Add(new UrlMenuItem("Another link to Admin",
        MenuPaths.Global + "/cms" + "/cmsMenuItem",
        Path.Combine(_modules.FindModule("EPiServer.CMS.UI.Admin").ResourceBasePath, "default")) {
        SortIndex = SortIndex.First + 25,
          AuthorizationPolicy = CmsPolicyNames.CmsAdmin
      });

      return menuItems;
    }
  }
}
```

### Add a second-level menu.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/rkcykIyunKJZp8BRiKLoE-20260722-194638.png "adminextension.png")

The example below adds a second-level menu to the existing Admin menu.

```csharp
public class ExtendedAdminController: Controller {
  [MenuItem("/global/cms/admin/extension", Text = "Admin Extension", SortIndex = 60)]
  public IActionResult Index() {
    return View();
  }
}
```

### New global menu section

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/8fBNZHpaOKDTfCqWJYcS6-20260722-194638.gif "examples-03-new-global-section.gif")

While the example below works, you should not use the main navigation for external links.

```csharp
using System.Collections.Generic;
using EPiServer.Security;
using EPiServer.Shell.Navigation;

namespace Alloy.Business {
  [MenuProvider]
  public class GlbalSectionMenuProvider: IMenuProvider {
    const string MainMenuPath = MenuPaths.Global + "/customSection";

    public IEnumerable<MenuItem> GetMenuItems() {
      var menuItems = new List<MenuItem>();

      menuItems.Add(new SectionMenuItem("Optimizely Forum", MainMenuPath) {
        SortIndex = SortIndex.Last + 10
      });

      menuItems.Add(new UrlMenuItem("CMS", MainMenuPath + "/item1",
        "https://world.episerver.com/forum/developer-forum/-Optimizely-75-CMS/") {
        SortIndex = 1,
      });

      menuItems.Add(new UrlMenuItem("Commerce", MainMenuPath + "/item2",
        "https://world.episerver.com/forum/developer-forum/Optimizely-Commerce/") {
        SortIndex = 2,
      });

      menuItems.Add(new UrlMenuItem("Forms", MainMenuPath + "/item3",
        "https://world.episerver.com/forum/developer-forum/episerver-forms/") {
        SortIndex = 3,
      });

      return menuItems;
    }
  }
}
```

### Extend the User Settings menu using controller action.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/lzdGp-PW0AG8CejEXecIR-20260722-194638.png "examples-04-extending-menu-using-controller-2.png")

```csharp
using EPiServer.Shell.Navigation;

namespace Alloy.Business {
  /// <summary>
  ///  Menu item under User Settings menu (menu added using controller action)
  /// </summary>
  /// <returns></returns>
  public class EpiServerWorldMenuItem: Controller {
    [MenuItem(MenuPaths.UserSettings, SortIndex = SortIndex.First - 10, Text = "About Optimizely",
      Url = "https://www.episerver.com/about/company/overview/")]
    public ActionResult Index(string viewName) {
      return View();
    }
  }
}
```

### Extend the User Settings menu using menu provider.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/i4-sjE36lJTnEg5BJHk4O-20260722-194638.png "examples-05-extending-menu-using-provider-2.png")

```csharp
using System.Collections.Generic;
using EPiServer.Shell.Navigation;

namespace Alloy.Business {
  [MenuProvider]
  public class UserSettingsMenuProvider: IMenuProvider {
    public IEnumerable<MenuItem> GetMenuItems() {
      var menuItems = new List<MenuItem>();
      var item = new UrlMenuItem("Documentation", MenuPaths.User + "/episerver2",
        "https://world.episerver.com/documentation/") {
        SortIndex = SortIndex.Last + 10
      };
      menuItems.Add(item);
      return menuItems;
    }
  }
}
```

### New drop-down menu with items

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/0Og0kEvYYZABkBb1huNA0-20260722-194638.png "text-action-links.png")

```csharp
using System.Collections.Generic;
using EPiServer.Shell.Navigation;

namespace Alloy.Business {
  [MenuProvider]
  public class DropdownMenuProvider: IMenuProvider {
    const string DropdownMenuPath = MenuPaths.Global + "/customDropdownMenu";

    public IEnumerable<MenuItem> GetMenuItems() {
      var menuItems = new List<MenuItem>();

      var userMenu = new DropDownMenuItem("Optimizely blogs", DropdownMenuPath) {
        SortIndex = SortIndex.Last - 20,
          Alignment = MenuItemAlignment.Right
      };
      menuItems.Add(userMenu);

      menuItems.Add(new UrlMenuItem("CMS", DropdownMenuPath + "/item1",
        "https://world.episerver.com/blogs/?type=cmsblog&page=1") {
        SortIndex = 1,
      });

      menuItems.Add(new UrlMenuItem("Commerce", DropdownMenuPath + "/item2",
        "https://world.episerver.com/blogs/?type=commerceblog&page=1") {
        SortIndex = 2,
      });

      menuItems.Add(new UrlMenuItem("Find", DropdownMenuPath + "/item3",
        "https://world.episerver.com/blogs/?type=findblog&page=1") {
        SortIndex = 3,
      });

      return menuItems;
    }
  }
}
```
