May 8th in JavaScript by .

Enhancing jQuery’s Hide Function For Greater Accessibility


Yesterday I read an article by Aaron Gustafson on A List Apart regarding hiding elements with JavaScript and the accessibility implications depending on the technique used. As mentioned by Aaron the common way this is done in libraries, including jQuery, is by manipulating the display property.

In the article Aaron discusses five ways to hide content and what the accessibility effect is of each of these. Out of the five there is only one that passes the accessibility checklist and that is:

position:absolute;
left:-99999em;

With this I decided to have a look at the jQuery source and see whether it would be possible to change, or enhance, the hide function to use this positioning as apposed to the current use of the display property. Turns out, it is.

The jQuery hide() Fucntion

hide: function(speed, easing, callback) {
	if (speed || speed === 0) {
		return this.animate(genFx("hide", 3), speed, easing, callback);

	} else {
		for (var i = 0, j = this.length; i < j; i++) {
			if (this[i].style) {
				var display = jQuery.css(this[i], "display");

				if (display !== "none" && !jQuery._data(this[i], "olddisplay")) {
					jQuery._data(this[i], "olddisplay", display);
				}
			}
		}

		// Set the display of the elements in a second loop
		// to avoid the constant reflow
		for (i = 0; i < j; i++) {
			if (this[i].style) {
				this[i].style.display = "none";
			}
		}

		return this;
	}
}

Looking at the hide function of jQuery above, there are two points in the code that is relevant to what we want to accomplish. The first part is this:

var display = jQuery.css(this[i], "display");

if (display !== "none" && !jQuery._data(this[i], "olddisplay")) {
	jQuery._data(this[i], "olddisplay", display);
}

First the current value of display is read from the element and stored in the display variable. Next, there is a check to see whether the element is already hidden and that there is no key called ‘olddisplay’ in the data object for this element. If both passes, a new entry is added storing the element’s current display state.

After this it enters another for loop setting the display of the element:

if (this[i].style) {
	this[i].style.display = "none";
}

This is then where the changes need to be made:

var position = jQuery.css( this[i], "position" ),
left = jQuery.css( this[i], "left" );

if ( left !== "-99999em" && !jQuery._data( this[i], "oldposition" ) ) {
	jQuery._data( this[i], {
		"oldposition" : position,
		"oldleft" : left
	});
}

For the changes we now need to track two properties, the current position as well as the left offset. The checks inside the if also needs to change so that we check for the left offset not being -99999em and that the ‘oldposition‘  key does not exist in the data object for the current element.

Again, if the above passes, we store these on the data object for the current element. Next, we move on to hide the element:

if ( this[i].style ) {
	this[i].style.position = "absolute";
	this[i].style.left = "-99999em";
}

So as mentioned before, and in the article, we are now using positioning to hide the element and not display.

The show() Function

However, it is not possible to change hide() in this way without affecting the show() function so next, we need to make some changes to show().

show: function(speed, easing, callback) {
	var elem, display;

	if (speed || speed === 0) {
		return this.animate(genFx("show", 3), speed, easing, callback);

	} else {
		for (var i = 0, j = this.length; i < j; i++) {
			elem = this[i];

			if (elem.style) {
				display = elem.style.display;

				// Reset the inline display of this element to learn if it is
				// being hidden by cascaded rules or not
				if (!jQuery._data(elem, "olddisplay") && display === "none") {
					display = elem.style.display = "";
				}

				// Set elements which have been overridden with display: none
				// in a stylesheet to whatever the default browser style is
				// for such an element
				if (display === "" && jQuery.css(elem, "display") === "none") {
					jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName));
				}
			}
		}

		// Set the display of most of the elements in a second loop
		// to avoid the constant reflow
		for (i = 0; i < j; i++) {
			elem = this[i];

			if (elem.style) {
				display = elem.style.display;

				if (display === "" || display === "none") {
					elem.style.display = jQuery._data(elem, "olddisplay") || "";
				}
			}
		}

		return this;
	}
}

The part to focus on here is the second loop:

if (elem.style) {
	display = elem.style.display;

	if (display === "" || display === "none") {
		elem.style.display = jQuery._data(elem, "olddisplay") || "";
	}
}

At this point we are not interested in the value of the display property anymore and instead want to have a look at the left offset. As above, if the checks pass we will set the properties of the element back to the previous values that we stored in the data object.

if ( elem.style ) {
	var left = elem.style.left;

	if ( left === "-99999em") {
		elem.style.position = jQuery._data(elem, "oldposition") || "";
		elem.style.left = jQuery._data(elem, "oldleft") || "";
	}
}

With that, we are done or, so I thought. Turns out, everything is fine, unless you pass a speed parameter, such as ‘slow’,  to the show() function. I had to do a little debugging to find the root of the problem but, the reason is that the animate function that is called when passing in the speed parameter is dependent on the state of the element being hidden.

With this not being the case anymore, simply nothing happens when animate is called. So here we are at a bit of a cross road. Change the animate function to check for the new properties and not display anymore or, abandon the idea. But wait, there is a third option. What if we set the element to be hidden just before we call the animate function? This will not hurt the accessibility of the element as it will have this state for a very small amount of time, literally less then a second.

To implement this look at the following lines in the show function:

if (speed || speed === 0) {
	return this.animate(genFx("show", 3), speed, easing, callback);
}

We need to add just one line to fix the problem:

if ( speed || speed === 0 ) {
	this[0].style.display = "none";
	return this.animate( genFx("show", 3), speed, easing, callback);

}

Problem is, now that we have set the element to display:none the element will keep this state and will not be set to visible at the end of the animation because we are not setting it nor are we storing it’s initial state in the data object. Let’s change that.

Revisiting the code that sets the properties of the element, we need to add another line here:

if ( left === "-99999em") {
	elem.style.display = jQuery._data(elem, "olddisplay") || "";
	elem.style.position = jQuery._data(elem, "oldposition") || "";
	elem.style.left = jQuery._data(elem, "oldleft") || "";
}

The above will not work however until we fix the hide function as well. In hide() we have to change two aspects. First we need to store the initial value in the data object again:

if ( this[i].style ) {
	var display = jQuery.css( this[i], "display" ),
	position = jQuery.css( this[i], "position" ),
	left = jQuery.css( this[i], "left" );

	if ( left !== "-99999em" && !jQuery._data( this[i], "oldposition" ) ) {
		jQuery._data( this[i], {
			"olddisplay" : display,
			"oldposition" : position,
			"oldleft" : left
		});
	}
}

One last thing to do. With the above, the display property will be set during the show function but will not be changed during hide(). So, to complete the change, change the ‘if’ statement of hide that sets the properties to the following:

if ( this[i].style ) {
	this[i].style.display = "";
	this[i].style.position = "absolute";
	this[i].style.left = "-99999em";
}

And with that, it is done. We now have a hide function that uses positioning as apposed to manipulating the display property. I have a simple test page over here showing that all of this still works, the JSLint tests all pass when building jQuery with these changes in there but, more testing will need to be done.

My fork of jQuery with this change is available on Github and I would love to get feedback and hear your comments on this change.

UPDATE: I removed my fork of jQuery with the change as I will be releasing a ’plugin’ in the next couple of days.

Image courtesy: nick wright planning

  • Anonymous

    Please check out my comment on AListApart – that this method of hiding is not a good idea: http://www.alistapart.com/comments/now-you-see-me/P20/#25

    • Anonymous

      Hey Jim, thank you for your comment.

      I did read your comment there and as Aaron also stated, true but, it is the ‘best’ way we currently have of doing this. With that said, and as mentioned in the my replies on Reddit, changing hide() with regards to jQuery would not be ideal and was not the intent.

      Instead, a function such as moveOffScreen with a matching :offscreen selector will make much more sense and can then be used in scenarios that warrant the use of this mechanism for hiding content.

  • Anonymous

    clicking “show” twice on the last 2 examples in the Demo page disables both show and hide. Tried this on Ubuntu Chromium

    • Anonymous

      Thanks for the feedback

  • Anonymous

    In a response to the comments on Reddit and to clear some things up, please see below:

    @curien I agree, the idea of this was never to change the functionality of jQuery and thus no pull request was issues nor a feature or change request made.My guess is, that if jQuery wanted to use this, there would be a moveOffScreen function that does this and not hide.

    I basically changed a forked version of jQuery to see whether this could be done and in a sense, it can be accomplished.With that said, I completely agree with spaceyraygun that this will break the :hidden selector, and for this reason, along with the moveOffScreen function there will be a :offscreen selector to check against.

    @onlyvotes – Going around calling people idiots really does not accomplish anything and constructive criticism will move everything along in a positive direction. Regarding, “All of these can be conveyed to the visually impaired in a meaningful way.” – If you are referring to ARIA, good point but, browser support is lacking in this regard. As for a sample use case, tabbed interfaces…..

    @kinghfb, @Oranu I would never want to break jQuery’s behavior and therefore it was never the intent to even attempt to get this merged into jQuery core. A plugin to use in specific use cases, such as tabbed interfaces, will make a lot more sense.

    In the end, I reckon the article title was misleading and, to be honest, I was not sure about it even after publishing, my bad. With that said, at least I got some valuable feedback and will take this into account moving forward with the work on the plugin ;)

  • http://ricardo.cc Ricardo

    Most times you also want to hide content from screen readers. It’s very uncommon to do the opposite, and:
    1. when you do, you can just .addClass(‘vhidden’), previously created for accessible hiding2. extend jQuery.fn.hideAccessible = function(){ return this.css({…}) }; which doesn’t mess with jQuery’s core

    • Anonymous

      Agree, as stated, there will be specific times when this mechanism of hiding will be preferred to display:none; So a more concise implementation is required.

Performance Optimization WordPress Plugins by W3 EDGE