Creating a social icons menu with SVGs in WordPress

For my latest WordPress theme, I modified the output of a standard WordPress menu so that at the frontend URLs to social media channels are displayed as SVG icons. In this post, I show you my solution.

Update from February 14, 2016: I recently tested the menu with a screen reader and noticed, that the SVG accessibility features are not very well supported. Because of that, I updated the post’s content.

Why SVG and not the easy to use icon fonts?

Icon fonts have some disadvantages in comparison to SVGs — not only, that they are sometimes rendered incorrectly. SVGs are more semantic and accessible than icon fonts. Sara Soueidan wrote a chapter about SVGs in the Smashing Book 5, this is one paragraph from it:

»SVGs are semantic. An SVG icon is a graphic — an image — so what better way to mark up an image than to use a (scalable vector) graphic tag? An SVG element (<svg>) represents an icon simply and semantically, while icon fonts usually require non-semantic markup like pseudo-elements and empty s to be displayed. For people concerned about semantics and accessibility this introduces a serious issue: these elements don’t accommodate screen readers well, not to mention that the markup generally just doesn’t make much sense — if any — for an image.«

For this reason, I did not want to use an icon font, but SVGs.

The solution for the SVG menu: a custom Walker_Nav_Menu class

The SVG file with the social media icons looks like that:

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="display: none;">
    <symbol id="icon-twitter" viewBox="0 0 16 16">
        <path d="M15,3.627c-0.515,0.229-1.068,0.383-1.649,0.452c0.592-0.355,1.048-0.918,1.262-1.589
	c-0.554,0.329-1.169,0.568-1.822,0.697c-0.524-0.558-1.271-0.907-2.098-0.907c-1.586,0-2.872,1.286-2.872,2.872
	c0,0.225,0.026,0.444,0.075,0.654c-2.387-0.12-4.504-1.263-5.921-3.001C1.728,3.229,1.586,3.724,1.586,4.25
	c0,0.996,0.506,1.875,1.277,2.39C2.393,6.625,1.95,6.496,1.562,6.281c0,0.013,0,0.024,0,0.037c0,1.392,0.989,2.552,2.304,2.816
	C3.625,9.199,3.372,9.234,3.11,9.234c-0.186,0-0.365-0.02-0.541-0.051c0.366,1.142,1.427,1.971,2.683,1.993
	c-0.982,0.771-2.222,1.23-3.566,1.23c-0.232,0-0.461-0.014-0.686-0.041c1.271,0.816,2.78,1.291,4.402,1.291
	c5.284,0,8.174-4.377,8.174-8.172c0-0.124-0.004-0.248-0.01-0.371C14.127,4.708,14.615,4.203,15,3.627z"/>
    </symbol>

    <symbol id="icon-feed" viewBox="0 0 16 16">
        <path d="M2,6v2c3.309,0,6,2.691,6,6h2C10,9.582,6.418,6,2,6z M2,2v2c5.514,0,10,4.486,10,10h2
		C14,7.373,8.627,2,2,2z M3.5,11C2.671,11,2,11.672,2,12.5S2.671,14,3.5,14S5,13.328,5,12.5S4.329,11,3.5,11z"/>
    </symbol>
</svg>

Theoretically, there is the possibility to insert a title element inside the svg — for example, to provide a title for screen reader users. That does not work in many combinations of screen readers and browsers. To provide an accessible title for the icons, we use a span element which is hidden via CSS. The desired SVG icon is shown with the help of the use element.

<svg class="icon-twitter">
   <use xlink:href="/pfad-zum-theme/svg/social-media-icons.svg#icon-twitter"/>
</svg>
<span class="screen-reader-text">Twitter</span>

The hash value at the end of the file path matches the ID of the icon’s symbol element. So we need this code with the right hash value inside each menu item. To modify the output of wp_nav_menu(), we can specify a Walker class.

if ( has_nav_menu( 'social' ) ) {
   wp_nav_menu(
      array(
         'theme_location' => 'social',
         'menu_class'     => 'social-menu',
         'container'      => '',
         'walker'         => new Svg_Social_Menu_Walker(),
         'depth'          => 1
      )
   );
}

We will create a child class of Walker_Nav_Menu because we only need to modify its start_el() method. This method controls the output of menu items. We can copy large parts of this method, and the following code shows only the modified parts with copied code lines for reference.

class Svg_Social_Menu_Walker extends Walker_Nav_Menu {
   // […]
   public function start_el( &$output, $item, $depth = 0,
      $args = array(), $id = 0 ) {
      // […]
      $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );

      $social_media_channels = array(
         'plus.google.com' => array(
            'id'                 => 'icon-google-plus',
            'screen-reader-text' => __( 'Google Plus', 'hannover' )
         ),
         'wordpress.org'   => array(
            'id'                 => 'icon-wordpress',
            'screen-reader-text' => __( 'WordPress.org', 'hannover' )
         ),
         'wordpress.com'   => array(
            'id'                 => 'icon-wordpress',
            'screen-reader-text' => __( 'WordPress.com', 'hannover' )
         ),
         'facebook.com'    => array(
            'id'                 => 'icon-facebook',
            'screen-reader-text' => __( 'Facebook', 'hannover' )
         ),
         'twitter.com'     => array(
            'id'                 => 'icon-twitter',
            'screen-reader-text' => __( 'Twitter', 'hannover' )
         ),
         'dribbble.com'    => array(
            'id'                 => 'icon-dribbble',
            'screen-reader-text' => __( 'Dribbble', 'hannover' )
         ),
         'pinterest.com'   => array(
            'id'                 => 'icon-pinterest',
            'screen-reader-text' => __( 'Pinterest', 'hannover' )
         ),
         'github.com'      => array(
            'id'                 => 'icon-github',
            'screen-reader-text' => __( 'GitHub', 'hannover' )
         ),
         'tumblr.com'      => array(
            'id'                 => 'icon-tumblr',
            'screen-reader-text' => __( 'Tumblr', 'hannover' )
         ),
         'youtube.com'     => array(
            'id'                 => 'icon-youtube',
            'screen-reader-text' => __( 'YouTube', 'hannover' )
         ),
         'flickr.com'      => array(
            'id'                 => 'icon-flickr',
            'screen-reader-text' => __( 'Flickr', 'hannover' )
         ),
         'vimeo.com'       => array(
            'id'                 => 'icon-vimeo',
            'screen-reader-text' => __( 'Vimeo', 'hannover' )
         ),
         'instagram.com'   => array(
            'id'                 => 'icon-instagram',
            'screen-reader-text' => __( 'Instagram', 'hannover' )
         ),
         'linkedin.com'    => array(
            'id'                 => 'icon-linkedin',
            'screen-reader-text' => __( 'LinkedIn', 'hannover' )
         ),
         'xing.de'         => array(
            'id'                 => 'icon-xing',
            'screen-reader-text' => __( 'Xing', 'hannover' )
         ),
         'xing.com'        => array(
            'id'                 => 'icon-xing',
            'screen-reader-text' => __( 'Xing', 'hannover' )
         ),
         '/feed'           => array(
            'id'                 => 'icon-feed',
            'screen-reader-text' => __( 'Feed', 'hannover' )
         ),
      );

Own code begins at $social_media_channels. Here we have to connect the URLs of the social networks with the IDs from the SVG file and the screen reader titles. We create an array of channel URL keys with value arrays, which include the respective ID and screen reader text for this channel.

$svg_id = "";

foreach ( $social_media_channels as $key => $value ) {
   $pattern = "|$key|";
   preg_match( $pattern, $atts['href'], $matches );
   if ( ! empty( $matches[0] ) ) {
      $match                  = $matches[0];
      $svg_id                 = $social_media_channels[ $match ]['id'];
      $svg_screen_reader_text = $social_media_channels[ $match ]['screen-reader-text'];
      break;
   }
}

In the next step, we save an empty string in a variable $svg_id and loop through the channel array. Inside the foreach loop, we can access the current array key with $key and the respective value (the array with ID and screen reader text) with $value. $atts['href'] holds the URL of the current menu item. We need to compare this value with the array key, so we save the pattern |$key|.

After that, we use preg_match() and pass the pattern, the menu item’s URL and a variable for matches as parameters. If $matches[0] is not empty, the current array key matches a part of the menu item’s URL. The value of this array position is $key, so the URL of the social media channel. If there is a match, we save this URL from $matches[0] in $match and fetch the ID from $social_media_channels[ $match ]['id']; and the screen reader text from $social_media_channels[ $match ]['screen-reader-text'];. After that, we break the loop.

if ( $svg_id != "" ) {
   $icon_url    = plugins_url( "svg/social-media-icons.svg#$svg_id", __DIR__ );
   $item_output = $args->before;
   $item_output .= '<a' . $attributes . '>';
   $item_output .= '<svg class="' . $svg_id . '"><use xlink:href="' . $icon_url . '"></use></svg><span class="screen-reader-text">' . $svg_screen_reader_text . '</span>';
   $item_output .= '</a>';
   $item_output .= $args->after;
} else {
   $item_output = $args->before;
   $item_output .= '<a' . $attributes . '>';
   $item_output .= $args->link_before . $title . $args->link_after;
   $item_output .= '</a>';
   $item_output .= $args->after;
}

Finally, we have to build the markup. If $svg_id is not empty, we can display an SVG. In this case, we store the URL from the SVG file in $icon_url and append the ID after a #. The following two lines are copied from the parent class. In the next line, we create an svg element with the ID as a class, so it can be styled. As the href attribute of the use element, we pass the URL and close the opened tags. After that, we wrap a span with the class screen-reader-text around the title. The else code is the default behavior.

Plugin

I created a small plugin that implements the feature as a widget. To use it, do the following after activation the plugin:

  1. Create a menu with social media links
  2. Choose »SVG Social Menu« as menu position
  3. Drag the widget into a widget area

Related posts

Leave a Comment

Your email address will not be published. Required fields are marked *