I wrote a snippet of code that allow me to replace dynamically a menu link with an icon, useful for dashboard type menu.
This trick use the Menu Attribute module and the Font-Awesome library - but it could also use any css based icon library.
Here I am sharing this snippet of code with you, but if you have an idea on how to improve it, please do not hesitate:

function THEME_menu_link(array $variables) {
  $element = $variables['element'];
  $sub_menu = '';
  
  $output = l($element['#title'], $element['#href'], $element['#localized_options']);
  
  if (isset($element['#original_link']['options']['attributes']['class'])) {
    $array_class = $element['#original_link']['options']['attributes']['class'];
    $count = count($array_class);
    
    for ($i=0; $i <= $count; $i++){ 
      if (substr($array_class[$i], 0, 5) == 'icon-' ){ // icon- is the prefix of all the icon classes
        return '<li' . drupal_attributes($element['#attributes']) . '><a href=""><i class="' . $array_class[$i] . '"></i></a></li>' . "\n";
      } 
    } 
  }
}
  1. With the Menu Attribute module, assign the class of the icon that you need i.e. .icon-search.
  2. Include the snippet of code to your template.php file - do not forget to replace THEME in the function name with the name of your theme i.e. bartik_menu_link
  3. If you are using another library than Font-Awesome, do no forget to change the class prefix - check the comments
  4. Enjoy!

Comments

lacliniquemtl’s picture

I just realized I didn't made my code as flexible as could be... Here you go with a better version :)

function THEME_menu_link(array $variables) {
  $element = $variables['element'];
  $sub_menu = '';
    
  $element['#attributes']['class'][] = 'level-' . $element['#original_link']['depth'];
  
  
  if (isset($element['#original_link']['options']['attributes']['class'])) {
    $array_class = $element['#original_link']['options']['attributes']['class'];
    $count = count($array_class);
    
    for ($i=0; $i <= $count; $i++){      
      if (substr($array_class[$i], 0, 5) == 'icon-' ){
        $icon = '<i class="' . $array_class[$i] . '"></i>';
        $output = '<a href="';
        if (isset($element['#original_link']['options']['attributes']['rel']) != 'nofollow') {
          $output .= $element['#href']; // If rel is set to 'nofollow' I don't need the link because I use it to activate Javascript. Remove these line if not a needed function...
        }
        
        $output .= '" title="' . $element['#title'] . '">' . $icon . '</a>';
        
        return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
      } 
    } 
  }
  else {
    $output = l($element['#title'], $element['#href'], $element['#localized_options']);
    
    if ($element['#below']) {
        $sub_menu = drupal_render($element['#below']);
      }
      
    return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
  }
}

jwilson3’s picture

I've taken what lacliniquemtl did above, and improved it quite a bit. Changes include:

  • the code is refactored and written in a way such that someone could extract it into a hook_preprocess_menu_link function and make a module out of this ;)
  • additional classes are no longer stripped from the icon menu item's anchor tag. For example, if you specify icon-foo another-class for the classes of the menu item and you'll get the following output:
    // Example output of the snippet below...
    <li class="leaf"><a href="/path-to-page" class="another-class"><i class="icon-foo"></i> <span>Foo</span></a></li>
    
  • all other attributes are still used on the anchor tag.
  • the Menu Link Text remains inside the link, but wrapped in a SPAN tag so you can easily hide it via CSS, for example:
    /* Example of how to hide a menu icon's text on small screens. */
    @media all and (max-width: 959px) {
      .icon-foo + span {
        overflow: hidden;
        position: absolute;
        clip: rect(0 0 0 0);
        height: 1px; width: 1px;
        margin: -1px; padding: 0; border: 0;
      }
    }
    

Here is the code...

/**
 * Implements theme_menu_link().
 *
 * This code adds an icon <I> tag for use with icon fonts when a menu item
 * contains a CSS class that starts with "icon-". You may add CSS classes to
 * your menu items through the Drupal admin UI with the menu_attributes contrib
 * module.
 *
 * Originally written by lacliniquemtl.
 * Refactored by jwilson3.
 * @see http://drupal.org/node/1689728
 */
function template_menu_link(array $variables) {
  $element = $variables['element'];

  // If there is a CSS class on the link that starts with "icon-", create
  // additional HTML markup for the icon, and move that specific classname there.
  if (isset($element['#original_link']['options']['attributes']['class'])) {
    foreach ($element['#original_link']['options']['attributes']['class'] as $key => $class) {

      if (substr($class, 0, 5) == 'icon-') {

        // We're injecting custom HTML into the link text, so if the original
        // link text was not set to allow HTML (the usual case for menu items),
        // we MUST do our own filtering of the original text with check_plain(),
        // then specify that the link text has HTML content.
        if (!isset($element['#original_link']['options']['html']) || empty($element['#original_link']['options']['html'])) {
          $element['#title'] = check_plain($element['#title']);
          $element['#localized_options']['html'] = TRUE;
        }

        // Create additional HTML markup for the link's icon element and wrap
        // the link text in a SPAN element, to easily turn it on or off via CSS.
        $element['#title'] = '<span class="' . $class . '"></span> <span>' . $element['#title'] . '</span>';

        // Finally, remove the icon class from link options, so it is not
        // printed twice.
        unset($element['#original_link']['localized_options']['attributes']['class'][$key]);
        $element['#localized_options']['attributes']['class'] = $element['#original_link']['localized_options']['attributes']['class'];

        //dpm($element); // For debugging.
      }
    }
  }

  // Save our modifications, and call core theme_menu_link().
  $variables['element'] = $element;
  return theme_menu_link($variables);
}

Edit April 7, 2014:

  • modified to use <span> instead of semantically incorrect <i>.
  • modified how we remove the class from the <a> tag, which causing weird issues with the Zen theme.
jeffglenn’s picture

This is fantastic! Just wanted to say thanks!

ijortengab’s picture

i improved your code, so we can add classes that use for setting,
example : class='icon-blabla icon-large';

function YOURTHEME_menu_link(array $variables) {	
  $element = $variables['element'];
  // These class used for settings only, so excluded
  // http:--->fortawesome.github.io/Font-Awesome/examples/
  $exclusion = array(
    'icon-large','icon-2x','icon-3x','icon-4x','icon-muted',
	'icon-border','icon-spin','icon-fixed-width','icon-li','icon-rotate-90',
	'icon-rotate-180','icon-rotate-270','icon-flip-horizontal','icon-flip-vertical','icon-stack',
	'icon-stack-base','icon-light'
  );

  if (isset($element['#original_link']['options']['attributes']['class'])) {	
    $i_classes = array();
    foreach ($element['#original_link']['options']['attributes']['class'] as $key => $class) {     
      if (substr($class, 0, 5) == 'icon-' && !in_array($class,$exclusion)) {
        if (!isset($element['#original_link']['options']['html']) || empty($element['#original_link']['options']['html'])) {
          $element['#title'] = check_plain($element['#title']);
          $element['#localized_options']['html'] = TRUE;
        }
		$i_classes[] = $class;
        unset($element['#localized_options']['attributes']['class'][$key]);        
      }	  
	  if (substr($class, 0, 5) == 'icon-' && in_array($class,$exclusion)) {
		$i_classes[] = $class;
		unset($element['#localized_options']['attributes']['class'][$key]);
	  }
    }
	if(!empty($i_classes)) {
		$element['#title'] = '<i'.drupal_attributes(array('class' => $i_classes)).'></i> <span>' . $element['#title'] . '</span>';
	}
  }
  $variables['element'] = $element;
  return theme_menu_link($variables);
}
jwilson3’s picture

The improvement above is a special case for FontAwesome (examples here), which provides the ability to alter icons using yet other icon classes, such as icon rotation (icon-rotate-90, icon-flip-vertical, etc), spinners (icon-spin), size (icon-large, icon-2x, icon-4x, etc), color (icon-muted), and border (icon-border).

But my question is does this work properly for *stacking* icons? Eg:

<li>
<span class="icon-stack">
  <i class="icon-circle icon-stack-base"></i>
  <i class="icon-flag icon-light"></i>
</span>
icon-flag on icon-circle<br>
</li>

Thanks.

ijortengab’s picture

I have made a module (sandbox) to wrap the menu item title with html code, so we can add tag font-icon.
See Documentation: http://bit.ly/15FzOKl
Sandbox : https://drupal.org/sandbox/m_roji28/2081231

driesdelaey’s picture

<?php
function MYAWESOMETHEME_menu_link(array $variables) {

 /**
 * Implements theme_menu_link().
 *
 * This code adds an icon <I> tag for use with icon fonts when a menu item
 * contains a CSS class that starts with "icon-". You may add CSS classes to
 * your menu items through the Drupal admin UI with the menu_attributes contrib
 * module.
 *
 * Originally written by lacliniquemtl.
 * Refactored by jwilson3 > mroji28 > driesdelaey.
 * @see http://drupal.org/node/1689728
 */

  $element = $variables['element'];
  $sub_menu = '';

  // If there is a CSS class on the link that starts with "icon-", create
  // additional HTML markup for the icon, and move that specific classname there.

  // Exclusion List for settings eg http://fontawesome.io/examples/
  $exclusion = array(
    'fa-lg','fa-2x','fa-3x','fa-4x','fa-5x',
    'fa-fw',
    'fa-ul', 'fa-li',
    'fa-border',
    'fa-spin',
    'fa-rotate-90', 'fa-rotate-180','fa-rotate-270','fa-flip-horizontal','fa-flip-vertical',
    'fa-stack', 'fa-stack-1x', 'fa-stack-2x',
    'fa-inverse'
  );


  if (isset($element['#original_link']['options']['attributes']['class'])) {

    foreach ($element['#original_link']['options']['attributes']['class'] as $key => $class) {

      if (substr($class, 0, 3) == 'fa-' && !in_array($class,$exclusion)) {

        // We're injecting custom HTML into the link text, so if the original
        // link text was not set to allow HTML (the usual case for menu items),
        // we MUST do our own filtering of the original text with check_plain(),
        // then specify that the link text has HTML content.

        if (!isset($element['#original_link']['options']['html']) || empty($element['#original_link']['options']['html'])) {
          $element['#title'] = check_plain($element['#title']);
          $element['#localized_options']['html'] = TRUE;
        }

        // Add the default-FontAwesome-prefix so we don't need to add it manually in the menu attributes
        $class = 'fa ' . $class;

        // Create additional HTML markup for the link's icon element and wrap
        // the link text in a SPAN element, to easily turn it on or off via CSS.
        $element['#title'] = '<i class="' . $class . '"></i> <span>' . $element['#title'] . '</span>';

        // Finally, remove the icon class from link options, so it is not printed twice.
        unset($element['#localized_options']['attributes']['class'][$key]);

        // kpr($element); // For debugging.

      }
    }
  }
  // Save our modifications, and call core theme_menu_link().
  $variables['element'] = $element;
  return theme_menu_link($variables);
}
?>
O U T L A W’s picture

The links used in menu could be used in breadcrumb too. But the code above works only for menu links, and will break if the icon will be used in breadcrumb. Using the theme_link() we provide a more generic solution. Where we don't want icons we can hide them using CSS.

Sorry for my english. ;)

/**
 * Implements theme_link().
 *
 * This code adds an icon <i> tag for use with icon fonts when a menu item
 * contains a CSS class that starts with "fa-". You may add CSS classes to
 * your menu items through the Drupal admin UI with the menu_attributes contrib
 * module.
 *
 * Originally written by lacliniquemtl.
 * Refactored by jwilson3 > mroji28 > driesdelaey > O U T L A W.
 * @see http://drupal.org/node/1689728
 */

function MYAWESOMETHEME_link (array $variables) {
  $attributes = $variables['options']['attributes'];

  // If there is a CSS class on the link that starts with "fa-", create
  // additional HTML markup for the icon, and move that specific classname there.

  // Exclusion List for settings eg http://fontawesome.io/examples/
  $exclusion = array(
    'fa-lg','fa-2x','fa-3x','fa-4x','fa-5x',
    'fa-fw',
    'fa-ul', 'fa-li',
    'fa-border',
    'fa-spin',
    'fa-rotate-90', 'fa-rotate-180','fa-rotate-270','fa-flip-horizontal','fa-flip-vertical',
    'fa-stack', 'fa-stack-1x', 'fa-stack-2x',
    'fa-inverse'
  );

  if (isset($attributes['class'])) {
    foreach ($attributes['class'] as $key => $class) {
      if (substr($class, 0, 3) == 'fa-' && !in_array($class,$exclusion)) {

        // We're injecting custom HTML into the link text, so if the original
        // link text was not set to allow HTML (the usual case for menu items),
        // we MUST do our own filtering of the original text with check_plain(),
        // then specify that the link text has HTML content.
        if (!isset($variables['options']['html']) || empty($variables['options']['html'])) {
          $variables['text'] = check_plain($variables['text']);
          $variables['options']['html'] = TRUE;
        }

        // Add the default-FontAwesome-prefix so we don't need to add it manually in the menu attributes
        $class = 'fa ' . $class;

        // Create additional HTML markup for the link's icon element and wrap
        // the link text in a SPAN element, to easily turn it on or off via CSS.
        $variables['text'] = '<i class="' . $class . '"></i> <span>' . $variables['text'] . '</span>';

        // Finally, remove the icon class from link options, so it is not printed twice.
        unset($variables['options']['attributes']['class'][$key]);
      }
    }
  }

  return theme_link($variables);
}
ice70’s picture

great code, thank you :)

// Create additional HTML markup for the link's icon element and wrap
// the link text in a SPAN element, to easily turn it on or off via CSS.
$variables['text'] = ' ' . $variables['text'] . '';

I think that should have some span tags?

// Create additional HTML markup for the link's icon element and wrap
// the link text in a SPAN element, to easily turn it on or off via CSS.
$variables['text'] = '<span>' . $variables['text'] . '</span>';
philsward’s picture

Sad to say, the latest implementation didn't end up working for me.  I had a bunch of 'foreach' warning errors on my UC product pages.

I did manage to find a super simple solution though which was my original thought on making a menu item work without having the text.

Check out: https://www.drupal.org/project/menu_html

This will allow you to paste the html for the fontawesome or glyphicon directly into the title of the link.

pipicom’s picture

Marvelous code! This should be a module by itself.

filtermusic.net

aanjaneyam’s picture

@ jwilson3 I am using zurb-foundation theme at https://drupal.org/project/zurb-foundation. I added your code to template.php of my theme which is in sites/sitename/themes directory (i am using aegir BOA by omega8.cc) and changed 'icon-' in your code to 'fi-' as foundation icons classes start with 'fi-' in my theme (example fi-home or fi-social-facebook). However it does not seem to do anything when I add 'fi-home' class in menu attributes. The icon does show before the menu text and firebub shows class .fi-home:before but "i" and "span" tags are not added as expected.

jwilson3’s picture

@amsri:

The icon does show before the menu text and firebug shows class .fi-home:before but "i" and "span" tags are not added as expected.

The above suggests that the code snippet is not being run at all. It looks like zurb-foundation has its own pre-existing theme function for theming menu_links (theme_zurb_foundation_menu_link) which means simply adding the code snippet above wont work. You need to add the snippet into the existing theme function.

If you can't figure out how to get this working with your theme, your best bet would be to try to get support on IRC or using a new discussion thread. This really isn't the place to diverge this discussion into theme-building support questions. The code seems to work fine for myself and various others who have continued to use and make improvements.

aanjaneyam’s picture

hi jwilson3 thanks for your reply. I had just one more question regarding your code. How should the following line of your code be amended to suit the foundation icon class names:

if (substr($class, 0, 5) == 'icon-') {

Should it be

if (substr($class, 0, 5) == 'fi-') {

OR

if (substr($class, 0, 3) == 'fi-') {

are there any other amendments to make.

jwilson3’s picture

It should be:

if (substr($class, 0, 3) == 'fi-') {

as for other changes, I do not know; you'll need to test this first to see if it works.

aanjaneyam’s picture

Ok the code is working now but only on non superfish menus. The problem was that it was not affecting the menu handled by superfish block (superfish module). It works only on non superfish menus. Any ideas to make it work on superfish menus rendered by superfish modules?

ispboy’s picture

great idea!

philsward’s picture

I haven't personally tried this module yet, but it looks like it is designed around this sort of topic:

https://www.drupal.org/project/icon

hotwebmatter’s picture

I recently had the occasion to research this topic, and my findings are summarized here.

I went with this expedient solution:

https://github.com/gnikolovski/link_alter

The developer has a blog post about his approach:

http://dev.studiopresent.com/blog/back-end/altering-menu-links

The crux of it (because link-only answers are discouraged) can be boiled down to this:

In Drupal 8 you can use the hook_link_alter(&$variables) hook to alter menu links. [...] But, there is no easy way to identify this menu link. You cannot tell which menu contains this link, so unless you want to alter all links on your site, using this hook to precisely target menu links is a bad idea.

Unless, you somehow add an identifier.

Fortunately, you can easily do just that and make all this more dynamic. You can use `form_alter()` hook to alter menu link edit form, and add something like this:

[field for entering FormattableMarkup, select list to determine where markup is added][1]

I found this method worked well with Font Awesome 5, which now uses JavaScript to inject SVG icons where appropriate markup is found, so you don't need to mess around with Sass mixins, Unicode values, CSS pseudo-selectors and all that nonsense any more.

Thanks to @sonfd for the tip in the comment, which might be even better:

https://www.drupal.org/project/fontawesome_menu_icons

Since I already solved my problem and I'm on a deadline, I might not check that module out right away. But it is good to know that it exists!

  [1]: https://i.stack.imgur.com/ngmeO.png