Accessible JQuery Toggles
Published: 16 May 2010
Last modified: 29 March 2011
Introduction
JQuery provides some great tools for easing the implementation of client-side Javascript functions. In this article, I discuss a technique which is commonly used to show or hide sections of a page. I then go on to discuss why this simple solution is problematic. Finally, I will build on the methods described to arrive at a solution.
If you want to see what’s going on behind the scenes in the following examples, I recommend viewing this page in Firefox web browser, with the Fangs screen reader emulator add-on, and the Firebug development tools add-on.
The simplistic solution
A common approach to applying the toggle effect to a container is to associate a Javascript click event with a heading, image, or both. In the first example, below, the click event is associated with a level 2 heading. To add a little visual sugar, the heading background image is set to a suitable graphic:
Clicking the mouse on the headings, or the associated graphic, will toggle the display of text.
Accessible JQuery Toggles Example 1
If the client does not have Javascript, or if it is disabled, all the entries will be expanded when the page loads. This conforms with the W3C Web Content Accessibility Guidelines, which state that pages should be “usable when scripts, applets, or other programmatic objects are turned off or not supported.” See checkpoint 6.3 for further details.
The markup
Here’s the relevant HTML for section 2 of the above example (I’ve trimmed some of the paragraph text):
<div class="wrapper">
<h2 class="toggle">Section 2</h2>
<div class="toggle">
<p>Proin accumsan congue aliquam…</p>
</div>
</div>
The wrapper div provides us with a container that can be formatted with CSS. Although it isn’t required for the toggle effect to work, it does allow the items that are to be toggled to be visually grouped together. In this example, I’ve added a light grey background, and thin grey border.
The Javascript
JQuery makes acheiving the toggle effect extremely simple. Here’s the JQuery code for the first example:
The scripts shown in this article are deliberately verbose. You would probably want to minify them for production use.
var toggle = function(event) {
event.preventDefault();
// Toggle the 'expanded' class of the h2.toggle
// element, then apply the slideToggle effect
// to div.toggle siblings.
$(this).toggleClass('expanded').
nextAll('div.toggle').slideToggle('fast');
};
$(document).ready (function (){
// Hide the toggle containers.
$('div.toggle').hide();
// Add a click event handler to all h2.toggle elements.
$('h2.toggle').click(toggle);
});
The $(document).ready function does two things. When the document is loaded, all the div elements with the toggle class are hidden. JQuery does this by setting their CSS display property to none:
// Hide the toggle container.
$('div.toggle').hide();
JQuery then adds a click event handler to all the h2 elements which have the toggle class. In this case, the toggle function is called:
The click event is triggered whenever the mouse is clicked whilst hovering over an element, or when an element has focus and the enter key is pressed.
// Add a click event handler to all h2.toggle elements.
$('h2.toggle').click(toggle);
Using the toggle function, the expanded class is added to, or removed from the h2 element. Then the display of all sibling div elements with the toggle class are toggled using the JQuery slideToggle function.
// Toggle the 'expanded' class of the h2.toggle
// element, then apply the slideToggle effect
// to div.toggle siblings.
$(this).toggleClass('expanded').
nextAll('div.toggle').slideToggle('fast');
The CSS
The CSS is very simple:
h2.toggle {
background-image: url(/images/plus.png);
background-repeat: no-repeat;
padding-left: 2em;
}
h2.expanded {
background-image: url(/images/minus.png);
}
All h2 elements with the toggle class are given the plus sign background image, and the text is nudged to the right to make way for the image. When the expanded class is added by JQuery, the CSS overrides the background image setting, replacing it with the minus sign image.
Good, but not good enough
This simple solution works, but it has its problems:
- There’s no way to toggle the sections without a mouse. The click event is triggered when the user clicks the h2 element with a mouse, or when the h2 element receives focus and Enter is pressed. Unfortunately, using the code in the first example, it is not possible for the h2 element to receive focus when using the keyboard.
- Content hidden with display: none is usually ignored by screen readers (see this article for more information), and there is no way to reveal the content, other than by clicking the element with a mouse.
- The background image is controlled by the toggle class on the h2 element. This is set in a static CSS file, so the image will always be present—even if Javascript is unavailable. Although not a serious problem, it gives a visual clue to functionality that doesn’t exist when Javascript is unavailable.
- Finally, there is no way to set the initial display state for sections at page load—sections are always hidden when the page is loaded. This may be the required effect most of the time, but sometimes it’s handy to be able to alter the initial state.
Fixing the problems
First, let’s dive straight in and have a look at the revised example:
Accessible JQuery Toggles Example 2
The revised example fixes all the above problems:
- The keyboard can be used to select and toggle the sections.
- A link element has been provided so that screen readers have something to work with.
- If there’s no Javascript available, the plus and minus symbols will not be displayed.
- It is possible to set the default display state at page load, as shown with Section 2.
The markup
Here’s the revised code that goes with this example. First, the HTML from Section 2:
<div class="wrapper">
<h2 class="_toggle _expanded">Section 2</h2>
<div class="_toggle">
<p>Proin accumsan congue aliquam…</p>
</div>
</div>
The only changes here are the addition of the _expanded class, and the underscore prefix of the _toggle class. The underscore prefixes are important, and will be explained later.
The CSS
Here’s the revised CSS (note the the underscores are not present here):
h2.toggle {
background-image: url(/images/plus.png);
background-repeat: no-repeat;
padding-left: 2em;
}
h2.expanded {
background-image: url(/images/minus.png);
}
h2.toggle a {
color: #000000;
}
h2.toggle a:focus {
background: #993235;
}
Other than the addition of the h2.toggle a and h2.toggle a:focus selectors, nothing has changed. All these selectors do is match the link colour to the usual h2 colour, and provide a background highlight when the link receives focus.
The Javascript
The HTML and CSS have remained quite simple. The bulk of the changes occur in the revised Javascript, making it easy to maintain clean HTML and CSS:
var create_name = function(text) {
// Convert text to lower case.
var name = text.toLowerCase();
// Remove leading and trailing spaces, and any non-alphanumeric
// characters except for ampersands, spaces and dashes.
name = name.replace(/^\s+|\s+$|[^a-z0-9&\s-]/g, '');
// Replace '&' with 'and'.
name = name.replace(/&/g, 'and');
// Replaces spaces with dashes.
name = name.replace(/\s/g, '-');
// Squash any duplicate dashes.
name = name.replace(/(-)+\1/g, "$1");
return name;
};
var add_link = function() {
// Convert the h2 element text into a value that
// is safe to use in a name attribute.
var name = create_name($(this).text());
// Create a name attribute in the following div.toggle
// to act as a fragment anchor.
$(this).next('div.toggle').attr('name', name);
// Replace the h2.toggle element with a link to the
// fragment anchor. Use the h2 text to create the
// link title attribute.
$(this).html(
'<a href="#' + name + '" title="Reveal ' +
$(this).text() + ' content">' +
$(this).html() + '</a>');
};
var toggle = function(event) {
event.preventDefault();
// Toggle the 'expanded' class of the h2.toggle
// element, then apply the slideToggle effect
// to div.toggle siblings.
$(this).
toggleClass('expanded').
nextAll('div.toggle').slideToggle('fast');
};
var remove_focus = function() {
// Use the blur() method to remove focus.
$(this).blur();
};
$(document).ready (function (){
// Replace the '_toggle' class with 'toggle'.
$('._toggle').
removeClass('_toggle').
addClass('toggle');
// Replace the '_expanded' class with 'expanded'.
$('._expanded').
removeClass('_expanded').
addClass('expanded');
// Hide all div.toggle elements that are not initially expanded.
$('h2.toggle:not(.expanded)').nextAll('div.toggle').hide();
// Add a link to each h2.toggle element.
$('h2.toggle').each(add_link);
// Add a click event handler to all h2.toggle elements.
$('h2.toggle').click(toggle);
// Remove the focus from the link tag when accessed with a mouse.
$('h2.toggle a').mouseup(remove_focus);
});
Yes, that looks like a lot of code, but there are a lot of comments in there. As with the first example, you’ll probably want to minify it for production use. The $(document).ready function is all you need for an overall understanding the logic.
The _toggle class is replaced with the toggle class. All this is does is remove the underscore prefix so that the class now matches the relevant CSS selector. If there’s no Javascript available, the _toggle class (with underscore) won’t match anything, so will have no effect.
// Replace the '_toggle' class with 'toggle'.
$('._toggle').
removeClass('_toggle').
addClass('toggle');
The same thing is done to the _expanded class:
// Replace the '_expanded' class with 'expanded'.
$('._expanded').
removeClass('_expanded').
addClass('expanded');
The default action for all div elements with the toggle class is to hide them. However, if they also contain the expanded class, they should remain expanded when the document loads. This is easy to achieve with a little help from the JQuery :not() selector:
// Hide all div.toggle elements that are not initially expanded.
$('h2.toggle:not(.expanded)').nextAll('div.toggle').hide();
Next, a link is added to the appropriate h2 headings (the add_link function will be explained in more detail soon). Adding a link to the headings serves two purposes: it makes the headings able to receive focus from the keyboard, and it provides a clear indication to screen readers that something else is available. By doing this in the Javascript code instead of the HTML, it becomes easier to write the HTML, and prevents the need for useless links when Javascript is not enabled:
// Add a link to each h2.toggle element.
$('h2.toggle').each(add_link);
As in the first example, a click event handler is added to all the appropriate h2 elements:
// Add a click event handler to all h2.toggle elements.
$('h2.toggle').click(toggle);
Finally, a mouseup event handler is added to the link in each h2.toggle element. Without this, the background highlight and focus border remain visible after the heading has been clicked with the mouse. This is what we want to happen with keyboard focus, but it looks odd when the mouse is being used:
// Remove the focus from the link tag when accessed with a mouse.
$('h2.toggle a').mouseup(remove_focus);
Creating links
The above explanation of the $(document).ready function should be enough to explain the steps that have been taken to resolve the problems I mentioned at the start of this article. All that’s left is to explain how the add_link function inserts links into the h2 elements which have the toggle class.
The text (Section 1, Section 2, Section 3) is extracted from the h2 element, and passed to the create_name function:
// Convert the h2 element text into a value that // is safe to use in a name attribute. var name = create_name($(this).text());
The create_name function uses regular expressions to remove invalid characters, replace spaces with dashes, and then replace duplicate dashes with single dashes. It could probably be tweaked a little, but it does the job well enough for now. As an example, if Section 1 is fed into the function, it will return section-1.
The output from the create_name function is then used to add a name attribute to the appropriate div element:
// Create a name attribute in the following div.toggle
// to act as a fragment anchor.
$(this).next('div.toggle').attr('name', name);
If the original div element looks like this:
<div class="toggle">
The addition of the name attribute will make it look like this:
<div class="toggle" name="section-1">
This gives the link element in the h2 element something to which it can link. An empty fragment identifier could be used (<a href=”#”>), but I prefer links to actually link to something.
With a suitable target in place, the text is extracted from the h2 element, and inserted into a link:
// Replace the h2.toggle element with a link to the // fragment anchor. Use the h2 text to create the // link title attribute. $(this).html( '<a href="#' + name + '" title="Reveal ' + $(this).text() + ' content">' + $(this).html() + '</a>');
Given the following h2 element:
<h2 class="toggle">Section 1</h2>
…the add_link function will return this:
<h2 class="toggle">
<a href="#section-1"
title="Reveal Section 1 content">
Section 1
</a>
</h2>
The end result
If we take a snippet of the original HTML, and add the _expanded class to section 2, we get this:
<div class="wrapper">
<h2 class="_toggle">Section 1</h2>
<div class="_toggle">
<p>Lorem ipsum dolor sit amet…</p>
</div>
</div>
<div class="wrapper">
<h2 class="_toggle _expanded">Section 2</h2>
<div class="_toggle">
<p>Proin accumsan congue aliquam…</p>
</div>
</div>
<div class="wrapper">
<h2 class="_toggle">Section 3</h2>
<div class="_toggle">
<p>Phasellus sapien est…</p>
</div>
</div>
With Javascript available, sections 1 and 2 will be contracted when the page loads, while section 2 will remain visible. If we run the revised Javascript on the above HTML, we end up with this:
<div class="wrapper">
<h2 class="toggle">
<a href="#section-1"
title="Reveal Section 1 content">
Section 1
</a>
</h2>
<div class="toggle" name="section-1">
<p>Lorem ipsum dolor sit amet…</p>
</div>
</div>
<div class="wrapper">
<h2 class="toggle expanded">
<a href="#section-2"
title="Reveal Section 2 content">
Section 2
</a>
</h2>
<div class="toggle" name="section-2">
<p>Proin accumsan congue aliquam…</p>
</div>
</div>
<div class="wrapper">
<h2 class="toggle">
<a href="#section-3"
title="Reveal Section 3 content">
Section 3
</a>
</h2>
<div class="toggle" name="section-1">
<p>Phasellus sapien est…</p>
</div>
</div>
More work required
Using links and fragment identifiers for their focus ability does create a problem that is not present in the simpler solution: the browser history becomes polluted with each click of a section heading. As the page URI does not change, this is quite annoying, and I’d like to find a solution for that too. Perhaps that will form another article in the near future.
29 March 2011
Update
Thanks to Stewart for pointing out (way bay in October 2010), that the browser history problem can be solved by simply adding return false; as the last statement of the toggle function.
This prevents the default action of the event from being triggered (it prevents the link anchor from being processed). That method works, but after a little research, it turns out that the event.preventDefault() method is the preferred solution. I have updated the code accordingly.
Conclusion
JQuery provides tools that make creating accessible Javascript driven web sites much simpler. Although some of the functions may not provide instant accessibility out-of-the-box, it doesn’t take much work to develop code that is accessible.
The revised code enables clean and simple HTML and CSS, which, rather than degrading gracefully when Javascript is not available, is gracefully upgraded when Javascript is present.
The extra code required to create this result is minimal and not overly complex. The Javascript can be packaged away in an external file, and pulled into your web page whenever it’s needed.
