Tooltip Menu

A simple tooltip menu where the submenu will either appear above or below the main menu, depending on available space.

TooltipMenu

This is a simple tooltip menu that will show a tooltip submenu when hovering over an item. The submenu will either appear above or below the main menu, depending on where more space is available. Using Modernizr’s touch detection, the menu will either react on hover or on click. Example media queries show how to adjust the style for smaller screens.

The HTML

<ul id="cbp-tm-menu" class="cbp-tm-menu">
	<li>
		<a href="#">Home</a>
	</li>
	<li>
		<a href="#">Veggie made</a>
		<ul class="cbp-tm-submenu">
			<li><a href="#" class="cbp-tm-icon-archive">Sorrel desert</a></li>
			<li><a href="#" class="cbp-tm-icon-cog">Raisin kakadu</a></li>
			<li><a href="#" class="cbp-tm-icon-location">Plum salsify</a></li>
			<li><a href="#" class="cbp-tm-icon-users">Bok choy celtuce</a></li>
			<li><a href="#" class="cbp-tm-icon-earth">Onion endive</a></li>
			<li><a href="#" class="cbp-tm-icon-location">Bitterleaf</a></li>
			<li><a href="#" class="cbp-tm-icon-mobile">Sea lettuce</a></li>
		</ul>
	</li>
	<li>
		<a href="#">Pepper tatsoi</a>
		<ul class="cbp-tm-submenu">
			<li><a href="#" class="cbp-tm-icon-archive">Brussels sprout</a></li>
			<li><a href="#" class="cbp-tm-icon-cog">Kakadu lemon</a></li>
			<li><a href="#" class="cbp-tm-icon-link">Juice green</a></li>
			<li><a href="#" class="cbp-tm-icon-users">Wine fruit</a></li>
			<li><a href="#" class="cbp-tm-icon-earth">Garlic mint</a></li>
			<li><a href="#" class="cbp-tm-icon-location">Zucchini garnish</a></li>
			<li><a href="#" class="cbp-tm-icon-mobile">Sea lettuce</a></li>
		</ul>
	</li>
	<li>
		<a href="#">Sweet melon</a>
		<ul class="cbp-tm-submenu">
			<li><a href="#" class="cbp-tm-icon-screen">Sorrel desert</a></li>
			<li><a href="#" class="cbp-tm-icon-mail">Raisin kakadu</a></li>
			<li><a href="#" class="cbp-tm-icon-contract">Plum salsify</a></li>
			<li><a href="#" class="cbp-tm-icon-pencil">Bok choy celtuce</a></li>
			<li><a href="#" class="cbp-tm-icon-article">Onion endive</a></li>
			<li><a href="#" class="cbp-tm-icon-clock">Bitterleaf</a></li>
		</ul>
	</li>
</ul>

The CSS

/* Iconfont made with icomoon.com */
@font-face {
	font-family: 'cbp-tmicons';
	src:url('../fonts/tmicons/cbp-tmicons.eot');
	src:url('../fonts/tmicons/cbp-tmicons.eot?#iefix') format('embedded-opentype'),
		url('../fonts/tmicons/cbp-tmicons.woff') format('woff'),
		url('../fonts/tmicons/cbp-tmicons.ttf') format('truetype'),
		url('../fonts/tmicons/cbp-tmicons.svg#cbp-tmicons') format('svg');
	font-weight: normal;
	font-style: normal;
}

/* reset  list style */
.cbp-tm-menu,
.cbp-tm-menu ul {
	list-style: none;
}

/* set menu position; change here to set to relative or float, etc. */
.cbp-tm-menu {
	display: block;
	position: absolute;
	z-index: 1000;
	bottom: 0;
	width: 100%;
	background: #47a3da;
	text-align: right;
	padding: 0 2em;
	margin: 0;
	text-transform: capitalize;
}

/* first level menu items */
.cbp-tm-menu > li {
	display: inline-block;
	margin: 0 2.6em;
	position: relative;
}

.cbp-tm-menu > li > a {
	line-height: 4em;
	padding: 0 0.3em;
	font-size: 1.2em;
	display: block;
	color: #fff;
}

.no-touch .cbp-tm-menu > li > a:hover,
.no-touch .cbp-tm-menu > li > a:active {
	color: #02639d;
}

/* sumbenu with transitions */
.cbp-tm-submenu {
	position: absolute;
	display: block;
	visibility: hidden;
	opacity: 0;
	padding: 0;
	text-align: left;
	pointer-events: none;
	-webkit-transition: visibility 0s, opacity 0s;
	-moz-transition: visibility 0s, opacity 0s;
	transition: visibility 0s, opacity 0s;
}

.cbp-tm-show .cbp-tm-submenu {
	width: 16em;
	left: 50%;
	margin: 0 0 0 -8em;
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	-webkit-transition: visibility 0s, opacity 0.3s;
	-moz-transition: visibility 0s, opacity 0.3s;
	transition: visibility 0s, opacity 0.3s;
}

.cbp-tm-show-above .cbp-tm-submenu {
	bottom: 100%;
	padding-bottom: 10px;
}

.cbp-tm-show-below .cbp-tm-submenu {
	top: 100%;
	padding-top: 10px;
}

/* extreme cases: not enough space on the sides */
.cbp-tm-nospace-right .cbp-tm-submenu {
	right: 0;
	left: auto;
}

.cbp-tm-nospace-left .cbp-tm-submenu {
	left: 0;
}

/* last menu item has to fit on the screen */
.cbp-tm-menu > li:last-child .cbp-tm-submenu {
	right: 0;
}

/* 	
arrow: depending on where the menu will be shown, we set 
the right position for the arrow
*/

.cbp-tm-submenu:after {
	border: solid transparent;
	content: " ";
	height: 0;
	width: 0;
	position: absolute;
	pointer-events: none;
}

.cbp-tm-show-above .cbp-tm-submenu:after {
	top: 100%;
	margin-top: -10px;
}

.cbp-tm-show-below .cbp-tm-submenu:after {
	bottom: 100%;
	margin-bottom: -10px;
}

.cbp-tm-submenu:after {
	border-color: transparent;
	border-width: 16px;
	margin-left: -16px;
	left: 50%;
}

.cbp-tm-show-above .cbp-tm-submenu:after {
	border-top-color: #fff;
}

.cbp-tm-show-below .cbp-tm-submenu:after {
	border-bottom-color: #fff;
}

.cbp-tm-submenu > li {
	display: block;
	background: #fff;
}

.cbp-tm-submenu > li > a {
	padding: 5px 2.3em 5px 0.6em; /* top/bottom paddings in 'em' cause a tiny "jump" in Chrome on Win */
	display: block;
	font-size: 1.2em;
	position: relative;
	color: #47a3da;
	border: 4px solid #fff;
	-webkit-transition: all 0.2s;
	-moz-transition: all 0.2s;
	transition: all 0.2s;
}

.no-touch .cbp-tm-submenu > li > a:hover,
.no-touch .cbp-tm-submenu > li > a:active {
	color: #fff;
	background: #47a3da;
}

/* the icons (main level menu icon and sublevel icons) */
.cbp-tm-submenu li a:before,
.cbp-tm-menu > li > a:before {
	font-family: 'cbp-tmicons';
	speak: none;
	font-style: normal;
	font-weight: normal;
	font-variant: normal;
	text-transform: none;
	line-height: 1;
	vertical-align: middle;
	margin-right: 0.6em;
	-webkit-font-smoothing: antialiased;
}

.cbp-tm-submenu li a:before {
	position: absolute;
	top: 50%;
	margin-top: -0.5em;
	right: 0.5em;
}

.cbp-tm-menu > li > a:not(:only-child):before {
	content: "f0c9";
	font-size: 60%;
	opacity: 0.3;
}

.cbp-tm-icon-archive:before {
	content: "e002";
}

.cbp-tm-icon-cog:before {
	content: "e003";
}

.cbp-tm-icon-users:before {
	content: "e004";
}

.cbp-tm-icon-earth:before {
	content: "e005";
}

.cbp-tm-icon-location:before {
	content: "e006";
}

.cbp-tm-icon-mobile:before {
	content: "e007";
}

.cbp-tm-icon-screen:before {
	content: "e008";
}

.cbp-tm-icon-mail:before {
	content: "e009";
}

.cbp-tm-icon-contract:before {
	content: "e00a";
}

.cbp-tm-icon-pencil:before {
	content: "e00b";
}

.cbp-tm-icon-article:before {
	content: "e00c";
}

.cbp-tm-icon-clock:before {
	content: "e00d";
}

.cbp-tm-icon-videos:before {
	content: "e00e";
}

.cbp-tm-icon-pictures:before {
	content: "e00f";
}

.cbp-tm-icon-link:before {
	content: "e010";
}

.cbp-tm-icon-refresh:before {
	content: "e011";
}

.cbp-tm-icon-help:before {
	content: "e012";
}

/* Media Queries */
@media screen and (max-width: 55.6875em) {
	.cbp-tm-menu {
		font-size: 80%;
	}
}

@media screen and (max-height: 25.25em), screen and (max-width: 44.3125em) {

	.cbp-tm-menu {
		font-size: 100%;
		position: relative;
		text-align: center;
		padding: 0;
		top: auto;
	}

	.cbp-tm-menu > li {
		display: block;
		margin: 0;
		border-bottom: 4px solid #3793ca;
	}

	.cbp-tm-menu > li:first-child {
		border-top: 4px solid #3793ca;
	}

	li.cbp-tm-show > a,
	.no-touch .cbp-tm-menu > li > a:hover,
	.no-touch .cbp-tm-menu > li > a:active {
		color: #fff;
		background: #02639d;
	}

	.cbp-tm-submenu {
		position: relative;
		display: none;
		width: 100%;
	}

	.cbp-tm-submenu > li {
		padding: 0;
	}

	.cbp-tm-submenu > li > a {
		padding: 0.6em 2.3em 0.6em 0.6em;
		border: none;
		border-bottom: 2px solid #6fbbe9;
	}

	.cbp-tm-submenu:after {
		display: none;
	}

	.cbp-tm-menu .cbp-tm-show .cbp-tm-submenu {
		display: block;
		width: 100%;
		left: 0;
		margin: 0;
		padding: 0;
		bottom: auto;
		top: auto;
	}
	
}

The JavaScript

/**
 * cbpTooltipMenu.js v1.0.0
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * 
 * Copyright 2013, Codrops
 * http://www.codrops.com
 */
;( function( window ) {
	
	'use strict';

	var document = window.document,
		docElem = document.documentElement;

	function extend( a, b ) {
		for( var key in b ) { 
			if( b.hasOwnProperty( key ) ) {
				a[key] = b[key];
			}
		}
		return a;
	}

	// from https://github.com/ryanve/response.js/blob/master/response.js
	function getViewportH() {
		var client = docElem['clientHeight'],
			inner = window['innerHeight'];
		if( client < inner )
			return inner;
		else
			return client;
	}

	// http://stackoverflow.com/a/11396681/989439
	function getOffset( el ) {
		return el.getBoundingClientRect();
	}

	// http://snipplr.com/view.php?codeview&id=5259
	function isMouseLeaveOrEnter(e, handler) { 
		if (e.type != 'mouseout' && e.type != 'mouseover') return false; 
		var reltg = e.relatedTarget ? e.relatedTarget : 
		e.type == 'mouseout' ? e.toElement : e.fromElement; 
		while (reltg && reltg != handler) reltg = reltg.parentNode; 
		return (reltg != handler); 
	}

	function cbpTooltipMenu( el, options ) {	
		this.el = el;
		this.options = extend( this.defaults, options );
		this._init();
	}

	cbpTooltipMenu.prototype = {
		defaults : {
			// add a timeout to avoid the menu to open instantly
			delayMenu : 100
		},
		_init : function() {
			this.touch = Modernizr.touch;
			this.menuItems = document.querySelectorAll( '#' + this.el.id + ' > li' );
			this._initEvents();
		},
		_initEvents : function() {
			
			var self = this;

			Array.prototype.slice.call( this.menuItems ).forEach( function( el, i ) {
				var trigger = el.querySelector( 'a' );
				if( self.touch ) {
					trigger.addEventListener( 'click', function( ev ) { self._handleClick( this, ev ); } );
				}
				else {
					trigger.addEventListener( 'click', function( ev ) {
						if( this.parentNode.querySelector( 'ul.cbp-tm-submenu' ) ) {
							ev.preventDefault();
						}
					} );
					el.addEventListener( 'mouseover', function(ev) { if( isMouseLeaveOrEnter( ev, this ) ) self._openMenu( this ); } );
					el.addEventListener( 'mouseout', function(ev) { if( isMouseLeaveOrEnter( ev, this ) ) self._closeMenu( this ); } );
				}
			} );

		},
		_openMenu : function( el ) {

			var self = this;
			clearTimeout( this.omtimeout );
			this.omtimeout = setTimeout( function() {
				var submenu = el.querySelector( 'ul.cbp-tm-submenu' );

				if( submenu ) {
					el.className = 'cbp-tm-show';
					if( self._positionMenu( el ) === 'top' ) {
						el.className += ' cbp-tm-show-above';
					}
					else {
						el.className += ' cbp-tm-show-below';
					}
				}
			}, this.touch ? 0 : this.options.delayMenu );

		},
		_closeMenu : function( el ) {
			
			clearTimeout( this.omtimeout );

			var submenu = el.querySelector( 'ul.cbp-tm-submenu' );

			if( submenu ) {
				// based on https://github.com/desandro/classie/blob/master/classie.js
				el.className = el.className.replace(new RegExp("(^|\s+)" + "cbp-tm-show" + "(\s+|$)"), ' ');
				el.className = el.className.replace(new RegExp("(^|\s+)" + "cbp-tm-show-below" + "(\s+|$)"), ' ');
				el.className = el.className.replace(new RegExp("(^|\s+)" + "cbp-tm-show-above" + "(\s+|$)"), ' ');
			}

		},
		_handleClick : function( el, ev ) {
			var item = el.parentNode,
				items = Array.prototype.slice.call( this.menuItems ),
				submenu = item.querySelector( 'ul.cbp-tm-submenu' )

			// first close any opened one..
			if( this.current &&  items.indexOf( item ) !== this.current ) {
				this._closeMenu( this.el.children[ this.current ] );
				this.el.children[ this.current ].querySelector( 'ul.cbp-tm-submenu' ).setAttribute( 'data-open', 'false' );
			}

			if( submenu ) {
				ev.preventDefault();

				var isOpen = submenu.getAttribute( 'data-open' );

				if( isOpen === 'true' ) {
					this._closeMenu( item );
					submenu.setAttribute( 'data-open', 'false' );
				}
				else {
					this._openMenu( item );
					this.current = items.indexOf( item );
					submenu.setAttribute( 'data-open', 'true' );
				}
			}

		},
		_positionMenu : function( el ) {
			// checking where's more space left in the viewport: above or below the element
			var vH = getViewportH(),
				ot = getOffset(el),
				spaceUp = ot.top ,
				spaceDown = vH - spaceUp - el.offsetHeight;
			
			return ( spaceDown <= spaceUp ? 'top' : 'bottom' );
		}
	}

	// add to global namespace
	window.cbpTooltipMenu = cbpTooltipMenu;

} )( window );

Tagged with:

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay up to date with the latest web design and development news and relevant updates from Codrops.