Instantly share code, notes, and snippets.

What would you like to do?
Select Navigation using window.onhashchange event with jQuery hashchange plugin
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Show current selected page in site-wide select navigation menu</title>
<link rel="stylesheet" href="css/style.css" media="screen" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="index, follow">
<meta name="description" content="Show current selected page in site-wide select navigation menu"/>
<link rel="author" href="">
<script>if ( 'querySelector' in document && 'addEventListener' in window && Array.prototype.forEach ) { document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/g, '') + ' js '; }</script>
<body class="home-page">
<div class="button dropdown"><form name="site-menu" action="#!" method="post">
<select name="menu-items" onchange="location = this.options[this.selectedIndex].value;" id="menu-select-menu" class="select-menu">
<option id="sm-top" value="#!"></option>
<option id="sm-home" value="">Home</option>
<option id="sm-blog" value="">Blog</option>
<option id="sm-members" value="">Members</option>
<option id="sm-forum" value="">Forum</option>
<option id="sm-login" value="">Login</option>
<option id="sm-contact" value="">Contact</option>
<option id="sm-about" value="">About</option>
<h1>Home page</h1>
<small><a href="">CSS Select</a>
by <a href="">Todd Parker</a></small>
<!-- <script src="//"></script>
<script>window.jQuery || document.write('<script src="../../../js/lib/jquery-1.11.1.min.js"><\/script>')</script> -->
<script src="../../../js/lib/jquery-1.11.1.min.js"></script>
<!-- <script src="js/"></script> -->
<script src="js/index.js"></script>
$('.home-page #sm-home').attr('selected',true).siblings('option').removeAttr('selected');
$('.blog-page #sm-blog').attr('selected',true).siblings('option').removeAttr('selected');
$('.members-page #sm-members').attr('selected',true).siblings('option').removeAttr('selected');
$('.forum-page #sm-forum').attr('selected',true).siblings('option').removeAttr('selected');
$('.login-page #sm-login').attr('selected',true).siblings('option').removeAttr('selected');
$('.contact-page #sm-contact').attr('selected',true).siblings('option').removeAttr('selected');
$('.about-page #sm-about').attr('selected',true).siblings('option').removeAttr('selected');
// fix for iOS
$('.home-page #sm-top').prepend('Home');
$('.blog-page #sm-top').prepend('Blog');
$('.members-page #sm-top').prepend('Members');
$('.forum-page #sm-top').prepend('Forum');
$('.login-page #sm-top').prepend('Login');
$('.contact-page #sm-top').prepend('Contact');
$('.about-page #sm-top').prepend('About');
* jQuery hashchange event - v1.3 - 7/21/2010
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
// Script: jQuery hashchange event
// *Version: 1.3, Last updated: 7/21/2010*
// Project Home -
// GitHub -
// Source -
// (Minified) - (0.8kb gzipped)
// About: License
// Copyright (c) 2010 "Cowboy" Ben Alman,
// Dual licensed under the MIT and GPL licenses.
// About: Examples
// These working examples, complete with fully commented code, illustrate a few
// ways in which this plugin can be used.
// hashchange event -
// document.domain -
// About: Support and Testing
// Information about what version or versions of jQuery this plugin has been
// tested with, what browsers it has been tested in, and where the unit tests
// reside (so you can test it yourself).
// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
// Unit Tests -
// About: Known issues
// While this jQuery hashchange event implementation is quite stable and
// robust, there are a few unfortunate browser bugs surrounding expected
// hashchange event-based behaviors, independent of any JavaScript
// window.onhashchange abstraction. See the following examples for more
// information:
// Chrome: Back Button -
// Firefox: Remote XMLHttpRequest -
// WebKit: Back Button in an Iframe -
// Safari: Back Button from a different domain -
// Also note that should a browser natively support the window.onhashchange
// event, but not report that it does, the fallback polling loop will be used.
// About: Release History
// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
// "removable" for mobile-only development. Added IE6/7 document.title
// support. Attempted to make Iframe as hidden as possible by using
// techniques from Added
// support for the "shortcut" format $(window).hashchange( fn ) and
// $(window).hashchange() like jQuery provides for built-in events.
// Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
// lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
// and <jQuery.fn.hashchange.src> properties plus document-domain.html
// file to address access denied issues when setting document.domain in
// IE6/7.
// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin
// from a page on another domain would cause an error in Safari 4. Also,
// IE6/7 Iframe is now inserted after the body (this actually works),
// which prevents the page from scrolling when the event is first bound.
// Event can also now be bound before DOM ready, but it won't be usable
// before then in IE6/7.
// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
// where browser version is incorrectly reported as 8.0, despite
// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
// window.onhashchange functionality into a separate plugin for users
// who want just the basic event & back button support, without all the
// extra awesomeness that BBQ provides. This plugin will be included as
// part of jQuery BBQ, but also be available separately.
'$:nomunge'; // Used by YUI compressor.
// Reused string.
var str_hashchange = 'hashchange',
// Method / object references.
doc = document,
special = $.event.special,
// Does the browser support window.onhashchange? Note that IE8 running in
// IE7 compatibility mode reports true for 'onhashchange' in window, even
// though the event isn't supported, so also test document.documentMode.
doc_mode = doc.documentMode,
supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 );
// Get location.hash (or what you'd expect location.hash to be) sans any
// leading #. Thanks for making this necessary, Firefox!
function get_fragment( url ) {
url = url || location.href;
return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
// Method: jQuery.fn.hashchange
// Bind a handler to the window.onhashchange event or trigger all bound
// window.onhashchange event handlers. This behavior is consistent with
// jQuery's built-in event handlers.
// Usage:
// > jQuery(window).hashchange( [ handler ] );
// Arguments:
// handler - (Function) Optional handler to be bound to the hashchange
// event. This is a "shortcut" for the more verbose form:
// jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
// all bound window.onhashchange event handlers will be triggered. This
// is a shortcut for the more verbose
// jQuery(window).trigger( 'hashchange' ). These forms are described in
// the <hashchange event> section.
// Returns:
// (jQuery) The initial jQuery collection of elements.
// Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
// $(elem).hashchange() for triggering, like jQuery does for built-in events.
$.fn[ str_hashchange ] = function( fn ) {
return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
// Property: jQuery.fn.hashchange.delay
// The numeric interval (in milliseconds) at which the <hashchange event>
// polling loop executes. Defaults to 50.
// Property: jQuery.fn.hashchange.domain
// If you're setting document.domain in your JavaScript, and you want hash
// history to work in IE6/7, not only must this property be set, but you must
// also set document.domain BEFORE jQuery is loaded into the page. This
// property is only applicable if you are supporting IE6/7 (or IE8 operating
// in "IE7 compatibility" mode).
// In addition, the <jQuery.fn.hashchange.src> property must be set to the
// path of the included "document-domain.html" file, which can be renamed or
// modified if necessary (note that the document.domain specified must be the
// same in both your main JavaScript as well as in this file).
// Usage:
// jQuery.fn.hashchange.domain = document.domain;
// Property: jQuery.fn.hashchange.src
// If, for some reason, you need to specify an Iframe src file (for example,
// when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
// do so using this property. Note that when using this property, history
// won't be recorded in IE6/7 until the Iframe src file loads. This property
// is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
// compatibility" mode).
// Usage:
// jQuery.fn.hashchange.src = 'path/to/file.html';
$.fn[ str_hashchange ].delay = 50;
$.fn[ str_hashchange ].domain = null;
$.fn[ str_hashchange ].src = null;
// Event: hashchange event
// Fired when location.hash changes. In browsers that support it, the native
// HTML5 window.onhashchange event is used, otherwise a polling loop is
// initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
// see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
// compatibility" mode), a hidden Iframe is created to allow the back button
// and hash-based history to work.
// Usage as described in <jQuery.fn.hashchange>:
// > // Bind an event handler.
// > jQuery(window).hashchange( function(e) {
// > var hash = location.hash;
// > ...
// > });
// >
// > // Manually trigger the event handler.
// > jQuery(window).hashchange();
// A more verbose usage that allows for event namespacing:
// > // Bind an event handler.
// > jQuery(window).bind( 'hashchange', function(e) {
// > var hash = location.hash;
// > ...
// > });
// >
// > // Manually trigger the event handler.
// > jQuery(window).trigger( 'hashchange' );
// Additional Notes:
// * The polling loop and Iframe are not created until at least one handler
// is actually bound to the 'hashchange' event.
// * If you need the bound handler(s) to execute immediately, in cases where
// a location.hash exists on page load, via bookmark or page refresh for
// example, use jQuery(window).hashchange() or the more verbose
// jQuery(window).trigger( 'hashchange' ).
// * The event can be bound before DOM ready, but since it won't be usable
// before then in IE6/7 (due to the necessary Iframe), recommended usage is
// to bind it inside a DOM ready handler.
// Override existing $.event.special.hashchange methods (allowing this plugin
// to be defined after jQuery BBQ in BBQ's source code).
special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
// Called only when the first 'hashchange' event is bound to window.
setup: function() {
// If window.onhashchange is supported natively, there's nothing to do..
if ( supports_onhashchange ) { return false; }
// Otherwise, we need to create our own. And we don't want to call this
// until the user binds to the event, just in case they never do, since it
// will create a polling loop and possibly even a hidden Iframe.
$( fake_onhashchange.start );
// Called only when the last 'hashchange' event is unbound from window.
teardown: function() {
// If window.onhashchange is supported natively, there's nothing to do..
if ( supports_onhashchange ) { return false; }
// Otherwise, we need to stop ours (if possible).
$( fake_onhashchange.stop );
// fake_onhashchange does all the work of triggering the window.onhashchange
// event for browsers that don't natively support it, including creating a
// polling loop to watch for hash changes and in IE 6/7 creating a hidden
// Iframe to enable back and forward.
fake_onhashchange = (function(){
var self = {},
// Remember the initial hash so it doesn't get triggered immediately.
last_hash = get_fragment(),
fn_retval = function(val){ return val; },
history_set = fn_retval,
history_get = fn_retval;
// Start the polling loop.
self.start = function() {
timeout_id || poll();
// Stop the polling loop.
self.stop = function() {
timeout_id && clearTimeout( timeout_id );
timeout_id = undefined;
// This polling loop checks every $.fn.hashchange.delay milliseconds to see
// if location.hash has changed, and triggers the 'hashchange' event on
// window when necessary.
function poll() {
var hash = get_fragment(),
history_hash = history_get( last_hash );
if ( hash !== last_hash ) {
history_set( last_hash = hash, history_hash );
$(window).trigger( str_hashchange );
} else if ( history_hash !== last_hash ) {
location.href = location.href.replace( /#.*/, '' ) + history_hash;
timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
return self;
/* */
/* Some basic page styles */
body {
font: 100%/1.5 AvenirNext-Regular, Corbel, "Lucida Grande", "Trebuchet Ms", sans-serif;
color: #111;
background-color: #fff;
margin: 2em 10%
/* Label styles: style as needed */
label {
margin: 2em 1em .25em .75em;
font-size: 1.25em;
/* Container used for styling the custom select, the buttom class adds the bg gradient, corners, etc. */
.dropdown {
position: relative;
/* This is the native select, we're making everything the text invisible so we can see the button styles in the wrapper */
.dropdown select {
border: 1px solid transparent;
outline: none;
/* Prefixed box-sizing rules necessary for older browsers */
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
/* Remove select styling */
appearance: none;
-webkit-appearance: none;
/* Magic font size number to prevent iOS text zoom */
/* General select styles: change as needed */
/* font-weight: bold; */
color: #444;
padding: .6em 1.9em .5em .8em;
.dropdown select,
label {
font-family: AvenirNextCondensed-DemiBold, Corbel, "Lucida Grande","Trebuchet Ms", sans-serif;
/* Custom arrow sits on top of the select - could be an image, SVG, icon font, etc. or the arrow could just baked into the bg image on the select */
.dropdown::after {
content: "";
position: absolute;
width: 9px;
height: 8px;
top: 50%;
right: 1em;
background-image: url(;
background-repeat: no-repeat;
background-size: 100%;
z-index: 2;
/* These hacks make the select behind the arrow clickable in some browsers */
/* This hides native dropdown button arrow in IE 10/11+ so it will have the custom appearance, IE 9 and earlier get a native select */
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.dropdown select::-ms-expand {
display: none;
/* Removes the odd blue bg color behind the text in IE 10/11 and sets the text to match the focus style text */
select:focus::-ms-value {
background: transparent;
color: #222;
/* Firefox >= 2 -- Older versions of FF (v2 - 6) won't let us hide the native select arrow, so we'll just hide the custom icon and go with native styling */
/* Show only the native arrow */
body:last-child .dropdown::after, x:-moz-any-link {
display: none;
/* reduce padding */
body:last-child .dropdown select, x:-moz-any-link {
padding-right: .8em;
/* Firefox 7+ -- Will let us hide the arrow, but inconsistently (see FF 30 comment below). We've found the simplest way to hide the native styling in FF is to make the select bigger than its container. */
/* The specific FF selector used below successfully overrides the previous rule that turns off the custom icon; other FF hacky selectors we tried, like `*>.dropdown::after`, did not undo the previous rule */
/* Set overflow:hidden on the wrapper to clip the native select's arrow, this clips hte outline too so focus styles are less than ideal in FF */
_::-moz-progress-bar, body:last-child .dropdown {
overflow: hidden;
/* Show only the custom icon */
_::-moz-progress-bar, body:last-child .dropdown:after {
display: block;
_::-moz-progress-bar, body:last-child .dropdown select {
/* increase padding to make room for menu icon */
padding-right: 1.9em;
/* `window` appearance with these text-indent and text-overflow values will hide the arrow FF up to v30 */
-moz-appearance: window;
text-indent: 0.01px;
text-overflow: "";
/* for FF 30+ on Windows 8, we need to make the select a bit longer to hide the native arrow */
width: 110%;
/* At first we tried the following rule to hide the native select arrow in Firefox 30+ in Windows 8, but we'd rather simplify the CSS and widen the select for all versions of FF since this is a recurring issue in that browser */
/* @supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal) {
.dropdown select { width:110%; }
} */
/* Firefox 7+ focus style - This works around the issue that -moz-appearance: window kills the normal select focus. Using semi-opaque because outline doesn't handle rounded corners */
_::-moz-progress-bar, body:last-child .dropdown select:focus {
outline: 2px solid rgba(180,222,250, .7);
/* Opera - Pre-Blink nix the custom arrow, go with a native select button */
x:-o-prefocus, .dropdown::after {
/* Hover style */
.dropdown:hover {
border:1px solid #888;
/* Focus style */
select:focus {
box-shadow: 0 0 1px 3px rgba(180,222,250, 1);
color: #222;
border:1px solid #aaa;
/* Firefox focus has odd artifacts around the text, this kills that */
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #000;
option {
/* These are just demo button-y styles, style as you like */
.button {
border: 1px solid #bbb;
border-radius: .3em;
box-shadow: 0 1px 0 1px rgba(0,0,0,.04);
background: #f3f3f3; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #e5e5e5 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#e5e5e5)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #ffffff 0%,#e5e5e5 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #ffffff 0%,#e5e5e5 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #ffffff 0%,#e5e5e5 100%); /* IE10+ */
background: linear-gradient(to bottom, #ffffff 0%,#e5e5e5 100%); /* W3C */
footer {
margin: 5em auto 3em;
padding: 2em 2.5%;
text-align: center;
a {
color: #c04;
text-decoration: none;
a:hover {
color: #903;
text-decoration: underline;

This comment has been minimized.


atelierbram commented Sep 18, 2014

When one is already using jQuery, easiest way is to use Ben Alman's great "hashchange plugin" to get a select-menu to display the right option when using a select-menu for site-wide navigation (maybe for small screens), for otherwise with each page refresh the selected option will jump to the top value when there is no selected attribute with a value of selected. This is how I would do it, probably could me less verbose, but it works:

  • set an id on all the individual option items, something like <option id="sm-home"> for the homepage <option id="sm-blog"> for the blog-page, and so on
  • for the next bit to work, we will have to add the page#target (to the id) to the end of the url of the hrefs option values, like so blog/index.html#sm-blog, or maybe blog/index.php#sm-blog
    <select name="menu-items" onchange="location = this.options[this.selectedIndex].value;" id="menu-select-menu" class="select-menu">
      <option id="sm-menu" value="">Menu</option>
      <option id="sm-home" value="">Home</option>
      <option id="sm-blog" value="">Blog</option>  
  • Bind an event to window.onhashchange (with the help of the plugin) that, when the hash changes, sets a class on the body with the same name as the #hash-tag
  • target the option which has the same/corresponding id as the class set on the body, setting the "selected" attribute as-appropriate.

  // Bind an event to window.onhashchange that, when the hash changes, sets the
  // class on the body with the same name as the #hash-tag
  $(window).hashchange( function(){
    var hash = location.hash;

    // add an class on the body with the same name as the #hash-tag
    $('body').addClass(' ' + ( hash.replace( /^#/, '' ) || 'blank' ));

    // target the option which has the same/corresponding id as the class set on the body, setting the "selected" attribute as-appropriate.
    $('.sm-home #sm-home').attr('selected',true).siblings('option').removeAttr('selected'); 
    $('.sm-blog #sm-blog').attr('selected',true).siblings('option').removeAttr('selected'); 

  // Since the event is only triggered when the hash changes, we need to trigger
  // the event now, to handle the hash the page may have loaded with.



Maybe I was overcomplicating things here a bit, and I guess you don't need the hashchange plugin after all (took a left turn, and got carried away a bit, and then rushed it, ... it happens).

If it is possible to set a class on the pages' body-tags (and why wouldn't that be possible?), and also give all those select options anid like so:

<option id="sm-blog" value="">Blog</option>  

Then you can target those options on the corresponding pages like this:

  $('.home-page #sm-home').attr('selected',true).siblings('option').removeAttr('selected'); 
  $('.blog-page #sm-blog').attr('selected',true).siblings('option').removeAttr('selected'); 

When on WordPress, one should really use the classes that are set by WordPress on the different body-tags. (one can check for this in Devtools/Firebug) ...

For example a blogroll-page can have this page-id-882 class set on the body-tag, so with jQuery javascript you can set the selected attribute on the option id="sm-blog" like this:

  $('.page-id-882 #sm-blog').attr('selected',true).siblings('option').removeAttr('selected'); 

Fix for iOS
This doesn't seem to work on iOS, at least on my iPad it always shows the most top option. To make this work in our advantage I tried the following: leave the top option, where it used to say "Menu", empty, and give it an id :

<option id="sm-top" value="#!"></option>

Now we can insert some text in this option with javascript, using the same classes on the body-tags we set earlier, using jQuery's .prepend method for "inside DOM Insertion":

  $('.home-page #sm-top').prepend('Home');
  $('.blog-page #sm-top').prepend('Blog'); 

Now also on iOS, we have our current page right in the top of the select-menu. On desktop it still goes to the option with the selected attribute, so having this extra option on top there may be redundant, but it wouldn't bother me that much.

P.S. This doesn't seem to work on iOS, at least on my iPad it always shows the most top option ... but there are all kinds of issues with select-menus on iOS, so well ... Fixed, see above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment