Adding HTML to a menu item in Drupal 8

In a previous blog post, I added a custom class to a menu item so we could show that a menu item was unpublished. This blog post builds on the code from that blog post.

In addition to showing sighted users when a menu item is unpublished, we also need to show it to screen readers. We figured the best solution is to add some text to the link and wrap it in a <span> visible only to screen readers. So, I did the following after adding the class for sighted users:

$item['title'] = '<span class="sr-only">unpublished </span>' . $item['title'];

This resulted in the HTML being escaped and displayed on the page:

Escaped HTML displaying in menu item.

It look me a few hours of Googling around for how to add markup to menu items and trying different things. I eventually found the answer in a comment of an old forum post that was about doing this in Drupal 4. It seems title can be plain text or it can be a Markup object. The following seems to work:

$item['title'] = \Drupal\Core\Render\Markup::create('<span class="sr-only">unpublished </span>' . $item['title']);

Of course, your theme will need to have an .sr-only class implemented that will hide the content appropriately.

The full solution in context:

 * Implements hook_preprocess_HOOK().
function mymodule_preprocess_menu(&$variables) {

  foreach ($variables['items'] as &$item) {

    // We only want to look at internal links.
    if (!$item['url']->isExternal() && $item['url']->isRouted()) {
      // RouteParameters gets the NID of the linked item.
      $routeParameters = $item['url']->getRouteParameters();
      $nid = $routeParameters['node'];

      // It is possible that an internal menu URL does not reference a node.
      if (!empty($nid)) {
        // Check the node status.
        $query = \Drupal::entityQuery('node');
        $query->condition('status', 0);
        $query->condition('nid', $nid);
        $entity = $query->execute();

        if (!empty($entity)) {
          // add .menu-unpublished css class
          // Add screen reader indication of unpublished state.
          $item['title'] = \Drupal\Core\Render\Markup::create('<span class="sr-only">unpublished </span>' . $item['title']);