← Back to Blog

Adding SVG icons to the WordPress Menu

SVG images are pretty cool. They can be resized to whatever dimensions you choose and they still look good on screen (being vector graphics), they’re smaller than jpegs/pngs, and you can modify them with css. That’s pretty sweet.

Using SVG icons instead of icon fonts is the direction you want to go in. You don’t have to deal with all that unpleasant <i class=’fa fa-icon’></i> markup and other such nonsense. There’s no messiness with unicodes mapping to different symbols in different browsers or languages, and they’re far more easy to make accessibility friendly, unlike icon fonts.

Naturally adding these to the WordPress menu would seem like a pretty natural step. Turns out this is slightly harder than it looks. Now there’s some plugins that will do this, but I like the option of being able to tailor things in the WordPress environment, so I decided to be difficult, and just do it myself, and create my own SVG menu icons plugin for my own site.

For this setup, I relied heavily on this helpful article about adding custom fields to the wordpress menu and this one for creating a custom walker.

Overview

  1. SVG Icon Setup
  2. Add custom fields to WordPress menu
  3. Create custom walker
  4. Rejoice!

1. SVG Icon Setup

First, we need to get an icon library. IcoMoon is your easiest go-to for icon fonts. You can create your own icon set for a project based on their free icons, purchase from their paid library, or add your own if you have a separate library.

Go to their generator, choose your icons, and then use the ‘Generate SVG and more’ to create your SVG set. This will create a demo file with all the SVG shapes The important files to take note of is symbol-defs.svg. This file contains all the definitions of the icon shapes. For example the home icon is defined as follows:

<symbol id="icon-home" viewBox="0 0 32 32">
<title>home</title>
<path d="M32 18.451l-16-12.42-16 12.42v-5.064l16-12.42 16 12.42zM28 18v12h-8v-8h-8v8h-8v-12l12-9z"></path>
</symbol>

To call the icon anywhere on the page, we then just need to reference the single file by the id:

 <svg class="icon icon-home"><use xlink:href="#icon-home"></use></svg>

Magic! You can input SVGs into your html directly, but keep all your icons in one place. The one downside to this is that IE doesn’t support the xlink functionality, so you’ll need the svgxuse.js polyfill. Edge does support it, however, so we’ll be able to drop that in the future.

So now we have our icon set. We’ll want to output the SVG syntax within the menu links. In order to do that (without javascript) we’ll need to add custom fields to the WordPress menu.

2. Add Custom Fields to the WordPress Menu

First, we need to create the plugin. Make a folder named menu-icons in wp-content/plugins and create the menu-icons.php file inside it. Add the basic plugin structure to the file, which looks like this:

<?php
/*
Plugin Name: SVG Icons
Plugin URI: Local Host
Description: Adds icon fields to menu items
Version: 1.0
Author: Eoin O'Dwyer
Author URI: http://eoinodwyer.com
*/



class mi_menuIcons{

	/*--------------------------------------------*
	 * Constructor
	 *--------------------------------------------*/

	/**
	 * Initializes the plugin by setting localization, filters, and administration functions.
	 */
	function __construct() {

		// add custom menu fields to menu
		add_filter( 'wp_setup_nav_menu_item', array( $this, 'mi_add_custom_nav_fields' ));


// instantiate plugin's class
$GLOBALS['menuIcons'] = new mi_menuIcons();

?>

Now we’re going to have to do three things: add a custom field to the menu items in the dashboard, save those custom field values, and then out put them in a separate walker. For this, we need to add three actions to the __construct() class:

// add custom menu fields to menu
add_filter( 'wp_setup_nav_menu_item', array( $this, 'mi_add_custom_nav_fields' ) );

// save menu custom fields
add_action( 'wp_update_nav_menu_item', array( $this, 'mi_update_custom_nav_fields'), 10, 3 );
	
// Add to menu walker
add_filter( 'wp_edit_nav_menu_walker', array( $this, 'mi_edit_walker'), 10, 2 );

Adding Custom Menu Fields to Menu

To add the field to the menu, we need to register a new input field with each menu item. We need to add the following function below the constructor, so it can be called by the first add_filter line:

/**
* Add custom fields to $item nav object
* in order to be used in custom Walker
*
* @access      public
* @since       1.0 
* @return      void
*/
function mi_add_custom_nav_fields( $menu_item ) {
    $menu_item->menuIcon = get_post_meta( $menu_item->ID, '_menu_item_menuIcon', true );
    return $menu_item;
}

Save Custom Values

Saving the values is a little more complex. We need to test that the values are properly sent, and then save to the post meta.

/**
	 * Save menu custom fields
	 *
	 * @access      public
	 * @since       1.0 
	 * @return      void
	*/
	function mi_update_custom_nav_fields( $menu_id, $menu_item_db_id, $args ) {

	    // Check if element is properly sent
	    if ( is_array( $_REQUEST['menu-item-menuIcon']) ) {
	        $menuIcon_value = $_REQUEST['menu-item-menuIcon'][$menu_item_db_id];
	        update_post_meta( $menu_item_db_id, '_menu_item_menuIcon', $menuIcon_value );
	    }

	}

Call the Custom Walker

To actually get the custom field to show up, we’re going to have to create an alternate nav menu walker. First, we call the new walker:

/**
* Define new Walker edit
*
* @access      public
* @since       1.0 
* @return      void
*/
function mi_edit_walker($walker,$menu_id) {
    return 'Walker_Nav_Menu_Edit_Custom';
}	  

Then, after the instantiation of the plugin class ($GLOBALS[‘menuIcons’] = new mi_menuIcons();), include a new walker file as follows:

include_once( 'edit_custom_walker.php' );

To create this new walker, copy wp-admin/includes/class-walker-nav-menu-edit.php to the plugin folder and rename it. Then, around line 159, you can add the following to get the icons to show up. For this snippet to work, you’ll need to put the SVG files you obtained from IcoMoon in a /fonts folder.

<p class="field-custom description description-wide">
<label for="edit-menu-item-menuicon-<?php echo $item_id; ?>">
<?php _e( 'Menu Icon' ); ?><br />
<input type="text" id="edit-menu-item-menuIcon-<?php echo $item_id; ?>" class="widefat code edit-menu-item-custom" name="menu-item-menuIcon[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $item->menuIcon ); ?>" />
</label>
<?php $themeDir = get_template_directory_uri().'/dist/fonts/demo.html'; ?>
<div>Choose icons from <a href="<?php echo plugins_url( 'fonts/demo.html', __FILE__ ) ?>">here</a></div>
</p>

Now when you go into the menu editor, you should the text field in which you can enter the SVG id, and a link to the demo file so you can choose the icons you desire:

3. Create Custom Walker

But wait! There’s more!

Now we have the custom field added to the menu editor, but we still need to get the SVGs into the menu. For this, we’ll need to create a custom menu walker, so as to change the html output of the menu.

Below the edit_custom_walker.php include statement add

include_once('menu_custom_walker.php');

and then create the file in the plugin folder and add the following to it:

 <?php

/**
*  Custom Menu Walker so can add icons
*/
class Icon_Walker extends Walker_Nav_Menu {

    var $number = 1;

    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';

        $class_names = $value = '';

        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'menu-item-' . $item->ID;

        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

        $output .= $indent . '<li' . $id . $value . $class_names .'>';

        // add svg icons here
         $themeDir = get_template_directory_uri();
         $theIcon = sprintf( '<svg class="icon icon-home" aria-labelledby="desc"><desc>Decorative Icon</desc></title><use xlink:href="'.plugins_url( 'fonts/symbol-defs.svg', __FILE__ ).'#'.esc_attr( $item->menuIcon ).'"></use></svg>');
        

        $atts = array();
        $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
        $atts['target'] = ! empty( $item->target )     ? $item->target     : '';
        $atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
        $atts['href']   = ! empty( $item->url )        ? $item->url        : '';

        $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args );

        $attributes = '';
        foreach ( $atts as $attr => $value ) {
            if ( ! empty( $value ) ) {
                $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
                $attributes .= ' ' . $attr . '="' . $value . '"';
            }
        }

        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $theIcon;
        $item_output .= '<span>'.$args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after . '</span>';
        $item_output .= '</a>';
        $item_output .= $args->after;

        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }

}
?>

Once you’ve added an icon class to one of your menus (let’s say icon-home), and saved your changes, this should now output html similar to the following:

<a href="http://localhost/portfolio/"><svg class="icon icon-home" aria-labelledby="desc"><desc>Decorative Icon</desc></title><use xlink:href="http://localhost/portfolio/wp-content/plugins/menu-icons/fonts/symbol-defs.svg#icon-home"></use></svg><span>Home</span></a>

And that’s it! You’re done. The theme for this site uses this setup to add the icons, so you can see how it performs. I’ll add a link to a repo of the final product when I’ve got more time.