Enhancing TabView Accessibility with WAI-ARIA Roles and States
July 30, 2008 at 4:11 pm by Todd Kloots | In Accessibility, Development | 13 CommentsThe YUI TabView Control is built on a strong foundation of semantic markup that provides users with some basic accessibility. But while TabView looks like a desktop tab control, screen readers don’t present it as an atomic widget, leaving users to figure out how the various HTML elements that compose a TabView relate to each other. However, through the application of the WAI-ARIA Roles and States, it is possible to enhance TabView’s accessibility such that users of screen readers perceive it as a desktop tab control.
A complete example of TabView using the WAI-ARIA Roles and States is available in the YUI Sandbox. Watch a screen cast of the example running in Firefox 3 with the NVDA screen reader, or download the latest development snapshot of NVDA and try it yourself.
Applying the WAI-ARIA Roles and States to TabView
Step 1: Getting Started
To start working with the WAI-ARIA Roles and States you’ll need both a browser and screen reader that support ARIA. Both Firefox 3 and Internet Explorer 8 Beta 1 have ARIA support. Trial versions of the leading JAWS and Window-Eyes screen readers are available for free download. However, the open-source NVDA Screen Reader is the best option for developers as it is both free and provides excellent support for ARIA.
Step 2: Adding Enhanced Keyboard Support
Out of the box, TabView provides basic keyboard support. Each Tab in a TabView is represented by
an <A> element whose href attribute is set to the id of an
<DIV> element that contains its content. In IE and Firefox,
<A> elements are automatically placed in the browser’s default tab index,
enabling the user to toggle between Tabs by pressing the tab key, and select a Tab by
pressing enter. (In Safari and Opera, <A> elements are not in the tab index
by default. Safari users can change this by going to the Safari Menu, Selecting “Preferences”,
then choose the “Advanced” tab and check the “Press Tab to highlight each item on a
webpage” checkbox.)
Having all of the Tabs in a TabView in the browser’s default tab index is a mixed blessing:
it provides basic keyboard accessibility, but can also make navigating more tedious in that users
navigating via the tab key have to tab through every Tab’s <A> as well as the
content of the active Tab’s correpsonding TabPanel in order to skip past the control. This problem
can be solved by setting the tabindex attribute of the <A> element
of the active Tab to a value of 0, and the inactive Tabs to -1. Setting an element’s
tabindex attribute to a value of -1 removes it from the browser’s default tab order,
while maintaining its focusability via JavaScript. Therefore, with this change in place it will be
easier for the user to skip over a TabView widget while navigating with the keyboard.
Mac vs. Windows
With only one Tab now in the browser’s default tab index, it will be necessary to supplement the TabView with support for the arrow keys to enable the user to navigate between Tabs as they would on the desktop. There are two different models for arrow key support for tabbed-content controls in operating systems: Mac OS X and Windows. On Windows, pressing the left or right arrow key moves focus to the next Tab and immediately displays its corresponding TabPanel. On the Mac, with VoiceOver enabled, the arrow keys only move focus between each Tab, and the user must press the space bar to load the content of the Tab’s corresponding TabPanel. Of the two, the Mac’s model might be considered better for a DHTML TabView. For example, if each Tab’s content is loaded via XHR, the Mac’s more intentional Tab selection model could help prevent the user from making requests for data he/she is not interested in consuming.
Supporting Multiple Orientations
The orientation
attribute of the TabView is used to render the Tabs on any of the widget’s four sides. To provide
arrow key support that will work regardless of the orientation of the Tabs, the left and up keys
will move the focus to the previous Tab, while the right and down arrow keys will move the focus to
the next Tab. As an additional convenience to the user, we’ll take another cue from the Mac’s
tab control implementation so that focus is automatically moved to the first or last Tab when the
user has reached the beginning or end of a list of Tabs.
To apply these keyboard enhancements to TabView, we’ll define a new prototype method named
enhanceAccessibility. This new method is designed to provide consistent keyboard
support for TabView across all of the A-Grade
browsers. It will work regardless of how the TabView is constructed (from existing markup, or
from script), its orientation, or if its content is static or loaded via XHR.
YAHOO.widget.TabView.prototype.enhanceAccessibility = function () {
var Dom = YAHOO.util.Dom,
Event = YAHOO.util.Event,
UA = YAHOO.env.ua,
oTabViewEl = this.get("element"),
oTabList = Dom.getChildren(oTabViewEl)[0],
aTabListItems = Dom.getChildren(oTabList),
aTabs = this.get("tabs"),
oTabIndexMap = {},
oTab,
oTabEl,
oTabAnchor,
oTabContentEl,
oFocusedTabAnchor,
sTabId,
oActiveTab;
// Set the "tabIndex" attribute of each Tab's <A> element: The
// "tabIndex" of the active Tab's <A> element is set to 0, the others to -1.
// This improves the keyboard accessibility of the TabView by placing
// only one Tab in the browser's tab index by default, allowing the user
// to easily skip over the control when navigating the page with the tab key.
Dom.batch(oTabList.getElementsByTagName("A"), function (element) {
element.tabIndex = -1;
});
oActiveTab = this.get("activeTab");
if (oActiveTab) {
Dom.getFirstChild(oActiveTab.get("element")).tabIndex = 0;
}
// Returns the <A> element representing each Tab in the TabView.
var getTabAnchor = function (element) {
var oTabAnchor;
if (Dom.getAncestorByClassName(element, "yui-nav")) {
if (element.nodeName.toUpperCase() === "A") {
oTabAnchor = element;
}
else {
oTabAnchor = Dom.getAncestorByTagName(element, "A");
}
}
return oTabAnchor;
};
// Keydown event listener for the TabView that provides support for
// using the arrow keys to move focus between each Tab.
this.on("keydown", function (event) {
var oCurrentTabAnchor = getTabAnchor(Event.getTarget(event)),
oCurrentTabLI,
oNextTabLI,
oNextTabAnchor;
if (oCurrentTabAnchor) {
oCurrentTabLI = oCurrentTabAnchor.parentNode;
switch (Event.getCharCode(event)) {
case 37: // Left
case 38: // Up
oNextTabLI = Dom.getPreviousSibling(oCurrentTabLI);
if (!oNextTabLI) {
oNextTabLI = aTabListItems[aTabListItems.length-1];
}
break;
case 39: // Right
case 40: // Down
oNextTabLI = Dom.getNextSibling(oCurrentTabLI);
if (!oNextTabLI) {
oNextTabLI = aTabListItems[0];
}
break;
}
oNextTabAnchor = Dom.getChildren(oNextTabLI)[0];
if (!oFocusedTabAnchor) {
oFocusedTabAnchor = oCurrentTabAnchor;
}
oFocusedTabAnchor.tabIndex = -1;
oNextTabAnchor.tabIndex = 0;
oNextTabAnchor.focus();
oFocusedTabAnchor = oNextTabAnchor;
}
});
};
Step 3: Adding the WAI-ARIA Roles and States
With the keyboard functionality in place, we’ll proceed with the application of the WAI-ARIA Roles and States. Once applied, assistive technologies (AT) such as a screen reader will no longer announce the HTML elements that compose the TabView as HTML elements, but as a tab control. In this way the relationship between the WAI-ARIA Roles and States and HTML is similar to that of CSS: both enable the developer to change the presentation of markup. And since the WAI-ARIA Roles and States enable the TabView to be presented to the user as a desktop tab control, it makes the previous work of applying desktop-like keyboard behavior all the more critical. If users of AT are going to preceive the TabView as a desktop tab control, it needs to fulfill that expectation from a keyboard perspective.
As a best practice, apply the WAI-ARIA Roles and States via JavaScript. Since the WAI-ARIA Roles and States depend on JavaScript-based keyboard functionality, it follows that the attributes representing the WAI-ARIA Roles and States only be applied via JavaScript. This Progressive Enhancement strategy ensures the best possible user experience by only applying WAI-ARIA Roles and States when the browser technologies required to support them (in this case, CSS and JavaScript) are available.
Roles and states are added to a TabView’s DOM elements via the
setAttribute
method. At present only two browsers have WAI-ARIA support:
Firefox 3 and
Internet Explorer 8 Beta 1.
(The changelog for Opera 9.5
mentions support for screen readers, MSAA, and ARIA, but in my testing in Opera I didn’t
find ARIA to work.) Therefore, we’ll make use of YUI’s browser detection
(YAHOO.env.ua) and
only apply the Roles and States to browsers that support them. The role of
tab will be applied to each Tab’s
<A> element, and the role of
tablist to their parent
<UL>. Finally, each Tab’s content element (<DIV>) will
receive the role of tabpanel and
an aria-labelledby attribute
with a value of the id of the <A> representing its corresponding Tab instance.
The aria-labelledby attribute enables the screen reader to announce the label of the
Tab for each TabPanel when the first element in a TabPanel receives focus, providing the user with
some context as to where they are. The following example illustrates how the ARIA roles and
properties are applied to each of the HTML elements that compose a TabView:
<div class="yui-navset"> <ul role="tablist"> <li> <a href="..." id="tab-1" role="tab">tab label</a> </li> </ul> <div clas="yui-content"> <div role="tabpanel" aria-labelledby="tab-1">tab content</div> </div> </div>
Screen-Reader Specific Tweaks
The implementation of the WAI-ARIA Roles and States is slightly different across screen readers, so
it is necessary to make some additional tweaks. A role of
presentation will need to be
applied to the parent <LI> element of each <A>, so that
the Window-Eyes screen reader recognizes that each Tab belongs to the same TabList. For JAWS it is
necessary to remove the href attribute of each Tab’s <A> element
to prevent it from announcing the attribute’s value when focused. Ideally JAWS would behave like
NVDA and Window-Eyes and allow the applied role attribute of
tab to take precedence over the
default role of the <A> element. The following illustrates the updated
markup for a TabView with the screen reader tweaks applied:
<div class="yui-navset"> <ul role="tablist"> <li role="presentation"> <a id="tab-1" role="tab">tab label</a> </li> </ul> <div clas="yui-content"> <div role="tabpanel" aria-labelledby="tab-1">tab content</div> </div> </div>
With this strategy for applying the WAI-ARIA Roles and States to TabView, we can update the
enhanceAccessibility method:
YAHOO.widget.TabView.prototype.enhanceAccessibility = function () {
var Dom = YAHOO.util.Dom,
Event = YAHOO.util.Event,
UA = YAHOO.env.ua,
oTabViewEl = this.get("element"),
oTabList = Dom.getChildren(oTabViewEl)[0],
aTabListItems = Dom.getChildren(oTabList),
aTabs = this.get("tabs"),
oTabIndexMap = {},
oTab,
oTabEl,
oTabAnchor,
oTabContentEl,
oFocusedTabAnchor,
sTabId,
oActiveTab;
// Set the "tabIndex" attribute of each Tab's <A> element: The
// "tabIndex" of the active Tab's <A> element is set to 0, the others to -1.
// This improves the keyboard accessibility of the TabView by placing
// only one Tab in the browser's tab index by default, allowing the user
// to easily skip over the control when navigating the page with the tab key.
Dom.batch(oTabList.getElementsByTagName("A"), function (element) {
element.tabIndex = -1;
});
oActiveTab = this.get("activeTab");
if (oActiveTab) {
Dom.getFirstChild(oActiveTab.get("element")).tabIndex = 0;
}
// Returns the <A> element representing each Tab in the TabView.
var getTabAnchor = function (element) {
var oTabAnchor;
if (Dom.getAncestorByClassName(element, "yui-nav")) {
if (element.nodeName.toUpperCase() === "A") {
oTabAnchor = element;
}
else {
oTabAnchor = Dom.getAncestorByTagName(element, "A");
}
}
return oTabAnchor;
};
// Keydown event listener for the TabView that provides support for
// using the arrow keys to move focus between each Tab.
this.on("keydown", function (event) {
var oCurrentTabAnchor = getTabAnchor(Event.getTarget(event)),
oCurrentTabLI,
oNextTabLI,
oNextTabAnchor;
if (oCurrentTabAnchor) {
oCurrentTabLI = oCurrentTabAnchor.parentNode;
switch (Event.getCharCode(event)) {
case 37: // Left
case 38: // Up
oNextTabLI = Dom.getPreviousSibling(oCurrentTabLI);
if (!oNextTabLI) {
oNextTabLI = aTabListItems[aTabListItems.length-1];
}
break;
case 39: // Right
case 40: // Down
oNextTabLI = Dom.getNextSibling(oCurrentTabLI);
if (!oNextTabLI) {
oNextTabLI = aTabListItems[0];
}
break;
}
oNextTabAnchor = Dom.getChildren(oNextTabLI)[0];
if (!oFocusedTabAnchor) {
oFocusedTabAnchor = oCurrentTabAnchor;
}
oFocusedTabAnchor.tabIndex = -1;
oNextTabAnchor.tabIndex = 0;
oNextTabAnchor.focus();
oFocusedTabAnchor = oNextTabAnchor;
}
});
// Only apply the WAI-ARIA Roles and States for FF 3 and IE 8 since those
// are the only browsers that currently support ARIA.
if ((UA.gecko && UA.gecko >= 1.9) || (UA.ie && UA.ie >= 8)) {
// Set the "role" attribute of the <UL> encapsulating the Tabs to "tablist"
oTabList.setAttribute("role", "tablist");
for (var i = 0, nLength = aTabs.length; i < nLength; i++) {
oTab = aTabs[i];
oTabEl = oTab.get("element");
oTabAnchor = Dom.getChildren(oTabEl)[0];
// Create a map that links the ids of each Tab's <A> element to
// the Tab's "index" attribute to make it possible to retrieve a Tab
// instance reference by id.
sTabId = oTabAnchor.id;
if (!sTabId) {
sTabId = Dom.generateId();
oTabAnchor.id = sTabId;
}
oTabIndexMap[sTabId] = i;
// Need to set the "role" attribute of each Tab's <LI> element to
// "presentation" so that Window-Eyes recognizes that each Tab belongs to
// the same TabList. Without this, Window-Eyes will announce each Tab as
// being "1 of 1" as opposed to "1 of 3," or "2 of 3".
oTabEl.setAttribute("role", "presentation");
oTabAnchor.setAttribute("role", "tab");
// JAWS announces the value of the "href" attribute of each Tab's <A>
// element when it recieves focus. Ideally JAWS would allow the
// applied "role" attribute of "tab" to take precedence over the default
// role of the <A> element like NVDA and Window-Eyes do. It is
// possible to fix this problem by removing the "href" attribute from
// the <A>.
oTabAnchor.removeAttribute("href");
oTabContentEl = oTab.get("contentEl");
oTabContentEl.setAttribute("role", "tabpanel");
// Set the "aria-labelledby" attribute for the TabPanel <LI> element to
// the id of its corresponding Tab's <A> element. Doing so enables the
// screen reader to announce the label of the Tab for each TabPanel when
// the first element in a TabPanel receives focus, providing the user
// with some context as to where they are.
oTabContentEl.setAttribute("aria-labelledby", sTabId);
}
// Add a keypress listener that toggles the active Tab instance when the user
// presses the Enter key. This is necessary because the removal of the "href"
// attribute from each Tab's <A> element (for JAWS support) causes the
// TabView's default Enter key support to stop working. Support for the Space
// Bar is also added as an additional convience for the user.
this.on("keypress", function (event) {
var oTabAnchor = getTabAnchor(Event.getTarget(event)),
nCharCode = Event.getCharCode(event);
if (oTabAnchor &&
(nCharCode === 13 || nCharCode === 32) &&
(oTabAnchor.parentNode !== this.get("activeTab").get("element"))) {
this.set("activeIndex", oTabIndexMap[oTabAnchor.id]);
}
});
}
};
Step 4: Putting It All Together
To test the new enhanceAccessibility method, we'll use the
Getting Content from an
External Source example from the existing TabView examples gallery as a starting point.
Once the TabView instance has been appended to the page, we'll call the new
enhanceAccessibility method. Next we'll use some additional WAI-ARIA Roles and States
to make some example-specific tweaks. First we'll, use the
describedby
property to provide some helpful instructional text that will be announced to the user
when the TabView initially receives focus. Since each Tab's content is loaded asynchronously, we'll
also leverage
WAI-ARIA Live Regions to
message users when a Tab's content is both being loaded and has finished loading.
(Note: The describedby property and Live Regions are currently only supported in the
latest development snapshots of NVDA.)
The following code snippet illustrates how it all comes together:
(function() {
var oTabView = new YAHOO.widget.TabView();
oTabView.addTab( new YAHOO.widget.Tab({
label: "Opera",
content: "<p>Please wait. Content loading.</p>",
dataSrc: "news.php?query=opera+browser",
cacheData: true,
active: true
}));
oTabView.addTab( new YAHOO.widget.Tab({
label: "Firefox",
content: "<p>Please wait. Content loading.</p>",
dataSrc: "news.php?query=firefox+browser",
cacheData: true
}));
oTabView.addTab( new YAHOO.widget.Tab({
label: "Explorer",
content: "<p>Please wait. Content loading.</p>",
dataSrc: "news.php?query=microsoft+explorer+browser",
cacheData: true
}));
oTabView.addTab( new YAHOO.widget.Tab({
label: "Safari",
content: "<p>Please wait. Content loading.</p>",
dataSrc: "news.php?query=apple+safari+browser",
cacheData: true
}));
oTabView.appendTo("container");
oTabView.enhanceAccessibility();
var Dom = YAHOO.util.Dom,
UA = YAHOO.env.ua,
oActiveTab,
oTitle,
oTabViewEl,
oLog,
sInstructionalText;
// Only apply the WAI-ARIA Roles and States for FF 3 and IE 8 since those
// are the only browsers that currently support ARIA.
if ((UA.gecko && UA.gecko >= 1.9) || (UA.ie && UA.ie >= 8)) {
oActiveTab = oTabView.get("activeTab");
// Append some instructional text to the <H2>
oTitle = Dom.get("tabview-title");
sInstructionalText = oTitle.innerHTML;
oTitle.innerHTML = (sInstructionalText + "<em id=\"tabview-description\">Press the space bar or enter key to load the content of each tab.</em>");
// Set the "aria-describedby" attribute of the <UL> with the role of "tablist"
// to the id of the <EM> inside the <H2>. This will trigger the screen reader
// to read the text of the <EM> when the TabView is initially focused,
// providing some additional instructional text to the user. (Currently this
// only works with the NVDA screen reader.)
Dom.getChildren(oTabView.get("element"))[0].setAttribute("aria-describedby", "tabview-description");
// Append a live region to the TabView's root element that will be used to
// message users about the status of the TabView.
oTabViewEl = oTabView.get("element");
oLog = oTabViewEl.ownerDocument.createElement("div");
oLog.setAttribute("role", "log");
oLog.setAttribute("aria-live", "polite");
oTabViewEl.appendChild(oLog);
// "activeTabChange" event handler used to notify the screen reader that
// the content of the Tab is loading.
oTabView.on("activeTabChange", function (event) {
var oTabEl = this.get("activeTab").get("element"),
sTabLabel = oTabEl.textContent || oTabEl.innerText,
oCurrentMessage = Dom.getFirstChild(oLog),
oMessage = oLog.ownerDocument.createElement("p");
oMessage.innerHTML = "Please wait. Content loading for " + sTabLabel + " property page.";
if (oCurrentMessage) {
oLog.replaceChild(oMessage, oCurrentMessage);
}
else {
oLog.appendChild(oMessage);
}
});
// "dataLoadedChange" event handler used to notify the screen reader that
// the content of the Tab has finished loading.
var onDataLoadedChange = function (event) {
var oTabEl = this.get("element"),
sTabLabel = oTabEl.textContent || oTabEl.innerText,
oCurrentMessage = Dom.getFirstChild(oLog),
oMessage = oLog.ownerDocument.createElement("p");
oMessage.innerHTML = "Content loaded for " + sTabLabel + " property page.";
if (oCurrentMessage) {
oLog.replaceChild(oMessage, oCurrentMessage);
}
else {
oLog.appendChild(oMessage);
}
};
oTabView.getTab(0).on("dataLoadedChange", onDataLoadedChange);
oTabView.getTab(1).on("dataLoadedChange", onDataLoadedChange);
oTabView.getTab(2).on("dataLoadedChange", onDataLoadedChange);
oTabView.getTab(3).on("dataLoadedChange", onDataLoadedChange);
}
})();
Further Reading and Resources
- Using WAI-ARIA Roles and States with the YUI Menu Control
- Accessible Rich Internet Applications (WAI-ARIA) Version 1.0 W3C Specification
- WAI-ARIA Best Practices
- ARIA: Accessible Rich Internet Applications/Relationship to HTML FAQ
- Accessible DHTML
- Key-navigable custom DHTML widgets
- Firefox 3
- Internet Explorer 8 Beta 1
- NVDA Screen Reader
- Freedom Scientific JAWS Screen Reader
- GW Micro Window-Eyes Screen Reader
- An Introduction to Screen Readers
- Making Ajax Work with Screen Readers
Share and extend: Bookmark with Yahoo! My Web | Bookmark with del.icio.us | digg it! | reddit!
AccordionView Widget for YUI from Marco van Hylckama Vlieg
July 25, 2008 at 6:08 am by Eric Miraglia | In Development, YUI Implementations | 20 Comments
Marco van Hylckama Vlieg, author of the YUI-based Dark Matter theme for Pixelpost (free version | pro version), is back with another outstanding contribution to the YUI ecosystem: The new AccordionView Widget.
We’ve written about a number of YUI-based accordions over the years, but this may be the most complete and the most consistent with YUI’s own widget style. Visually and syntactically, Marco’s accordion feels part and parcel with other YUI widgets you may be using.
Markup for the widget begins with an unordered list:
<ul id="mymenu" class="yui-accordionview">
<li class="yui-accordion-panel">
<a class="yui-accordion-toggle" href="#">Item 1</a>
<div class="yui-accordion-content">
<!--content goes here-->
</div>
</li>
<li class="yui-accordion-panel">
<a class="yui-accordion-toggle" href="#">Item 2</a>
<div class="yui-accordion-content">
<!--content goes here-->
</div>
</li>
</ul>
Once markup is in place, you can instantiate the widget:
var accordion = new YAHOO.widget.AccordionView('mymenu', options);
According to Marco’s documentation page, AccordionView supports the following options:
- width: css value for width including unit (example: ‘400px’, ‘15em’, etc.)
- expandItem: index of item to expand at initialization, default is none. 0 is the first, 1 the second, etc.
- animationSpeed: speed in seconds for animation. The default is 0.7
- animate: true or false, default is true
- effect: YUI Animation effect to use on animation. See the documentation on YAHOO.util.Easing. Default is YAHOO.util.Easing.easeBoth
- collapsible: true or false, whether the whole thing can be collapsed or not, default is true
- expandable: true or false, whether the whole thing can expand (true) or act as an accordion where only one item can be open (false). default is false
- hoverActivated: true or false, when set to true, the items are activated on hover. Note that this activates on click ALSO in order to keep keyboard navigation working.
Marco calls AccordionView a “work in progress”; you can give him feedback on how it’s working for you and what features you’d like to see added or refined on his blog.
Share and extend: Bookmark with Yahoo! My Web | Bookmark with del.icio.us | digg it! | reddit!
Non-blocking JavaScript Downloads
July 22, 2008 at 10:41 am by Stoyan Stefanov | In Development, Performance | 32 Comments
About the Author: Stoyan Stefanov is a Yahoo! web developer working for the Exceptional Performance team and leading the development of the YSlow performance tool. He also an open-source contributor, conference speaker and technical writer: his latest book is called Object-Oriented JavaScript.
External JavaScript files block downloads and hurt your page performance, but there is an easy way to work around this problem: use dynamic scripts tags and load scripts in parallel, improving the page loading speed and the user experience.
The problem: scripts block downloads
Let’s first take a look at what the problem is with the script downloads. The thing is that before fully downloading and parsing a script, the browser can’t tell what’s in it. It may contain document.write() calls which modify the DOM tree or it may even contain location.href and send the user to a whole new page. If that happens, any components downloaded from the previous page may never be needed. In order to avoid potentially useless downloads, browsers first download, parse and execute each script before moving on with the queue of other components waiting to be downloaded. As a result, any script on your page blocks the download process and that has a negative impact on your page loading speed.
Here’s how the timeline looks like when downloading a slow JavaScript file (exaggerated to take 1 second). The script download (the third row in the image) blocks the two-by-two parallel downloads of the images that follow after the script:
Here’s the example to test yourself.
Problem 2: number of downloads per hostname
Another thing to note in the timeline above is how the images following the script are downloaded two-by-two. This is because of the restriction of how many components can be downloaded in parallel. In IE <= 7 and Firefox 2, it’s two components at a time (following the HTTP 1.1 specs), but both IE8 and FF3 increase the default to 6.
You can work around this limitation by using multiple domains to host your components, because the restriction is two components per hostname. For more information of this topic check the article “Maximizing Parallel Downloads in the Carpool Lane” by Tenni Theurer.
The important thing to note is that JavaScripts block downloads across all hostnames. In fact, in the example timeline above, the script is hosted on a different domain than the images, but it still blocks them.
Scripts at the bottom to improve user experience
As Yahoo!’s Performance rules advise, you should put the scripts at the bottom of the page, towards the closing </body> tag. This doesn’t really make the page load faster (the script still has to load), but helps with the progressive rendering of the page. The user perception is that the page is faster when they can see a visual feedback that there is progress.
Non-blocking scripts
It turns out that there is an easy solution to the download blocking problem: include scripts dynamically via DOM methods. How do you do that? Simply create a new <script> element and append it to the <head>:
var js = document.createElement('script');
js.src = 'myscript.js';
var head = document.getElementsByTagName('head')[0];
head.appendChild(js);
Here’s the same test from above, modified to use the script node technique. Note that the third row in the image takes just as long to download, but the other resources on the page are loading simultaneously:
As you can see the script file no longer blocks the downloads and the browser starts fetching the other components in parallel. And the overall response time is cut in half.
Dependencies
A problem with including scripts dynamically would be satisfying the dependencies. Imagine you’re downloading 3 scripts and three.js requires a function from one.js. How do you make sure this works?
Well, the simplest thing is to have only one file, this way not only avoiding the problem, but also improving performance by minimizing the number of HTTP requests (performance rule #1).
If you do need several files though, you can attach a listener to the script’s onload event (this will work in Firefox) and the onreadystatechange event (this will work in IE). Here’s a blog post that shows you how to do this. To be fully cross-browser compliant, you can do something else instead: just include a variable at the bottom of every script, as to signal “I’m ready”. This variable may very well be an array with elements for every script already included.
Using YUI Get utility
The YUI Get Utility makes it easy for you to use script includes. For example if you want to load 3 files, one.js, two.js and three.js, you can simply do:
var urls = ['one.js', 'two.js', 'three.js']; YAHOO.util.Get.script(urls);
YUI Get also helps you with satisfying dependencies, by loading the scripts in order and also by letting you pass an onSuccess callback function which is executed when the last script is done loading. Similarly, you can pass an onFailure function to handle cases where scripts fail to load.
var myHandler = {
onSuccess: function(){
alert(':))');
},
onFailure: function(){
alert(':((');
}
};
var urls = ['1.js', '2.js', '3.js'];
YAHOO.util.Get.script(urls, myHandler);
Again, note that YUI Get will request the scripts in sequence, one after the other. This way you don’t download all the scripts in parallel, but still, the good part is that the scripts are not blocking the rest of the images and the other components on the page. Here’s a good example and tutorial on using YUI Get to load scripts.
YUI Get can also include stylesheets dynamically through the method
YAHOO.util.Get.css() [example].
Which brings us to the next question:
And what about stylesheets?
Stylesheets don’t block downloads in IE, but they do in Firefox. Applying the same technique of dynamic inserts solves the problem. You can create dynamic link tags like this:
var h = document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.href = 'mycss.css';
link.type = 'text/css';
link.rel = 'stylesheet';
h.appendChild(link);
This will improve the loading time in Firefox significantly, while not affecting the loading time in IE.
Another positive side effect of the dynamic stylesheets (in FF) is that it helps with the progressive rendering. Usually both browsers will wait and show blank screen until the very last piece of stylesheet information is downloaded, and only then they’ll start rendering. This behavior saves them the potential work of re-rendering when new stylesheet rules come down the wire. With dynamic <link>s this is not happening in Firefox, it will render without waiting for all the styles and then re-render once they arrive. IE will behave as usual and wait.
But before you go ahead and implement dynamic <link> tags, consider the violation of the rule of separation of concerns: your page formatting (CSS) will be dependent on behavior (JS). In addition, this problem is going to be addressed in future Firefox versions.
Other ways?
There are other ways to achieve the non-blocking scripts behavior, but they all have their drawbacks.
| Method | Drawback |
|---|---|
Using defer attribute of the script tag |
IE-only, unreliable even there |
Using document.write() to write a script tag |
|
XMLHttpRequest to get the source then execute with eval(). |
|
| XHR request to get the source, create a new script tag and set its content |
|
| Load script in an iframe |
|
Future
Safari and IE8 are already changing the way scripts are getting loaded. Their idea is to download the scripts in parallel, but execute them in the sequence they’re found on the page. It’s likely that one day this blocking problem will become negligible, because only a few users will be using IE7 or lower and FF3 or lower. Until then, a dynamic script tag is an easy way around the problem.
Summary
- Scripts block downloads in FF and IE browsers and this makes your pages load slower.
- An easy solution is to use dynamic
<script>tags and prevent blocking. - YUI Get Utility makes it easier to do script and style includes and manage dependencies.
- You can use dynamic
<link>tags too, but consider the separation of concerns first.
Share and extend: Bookmark with Yahoo! My Web | Bookmark with del.icio.us | digg it! | reddit!
Performance Research, Part 6: Less is More — Serving Files Faster by Combining Them
July 21, 2008 at 8:03 am by Tenni Theurer | In Performance | 15 CommentsIn Performance Research Part 1, we discussed how reducing the number of HTTP requests has the biggest impact on improving the response time and is often the easiest performance improvement to make. One technique without having to simplify the page design is to combine multiple scripts into a single script, and similarly combine multiple stylesheets into a single stylesheet.
Combining multiple files reduces the extra bytes from HTTP headers as well as potential transfer latency caused by TCP slow starts, packet losses, etc.
Figure 1 shows a graphical view of how time is spent loading a page with six separate scripts. Notice that for every file, the browser makes a separate HTTP request to retrieve the file. The gaps between the scripts indicate the time the browser takes to parse and render each script. Figure 2 shows the how time is spent loading a page with the same six scripts combined into a single script.
Figure 1. Loading a page with six separate scripts

Figure 2. Loading a page with one combined script

Combining JavaScript and CSS files as part of the development process can be burdensome. It usually makes sense during development to organize the code into logical modules as separate files. Typically, combining those separate files before product release is either a manual process or part of a build process. Every time one of the individual files is changed, the larger file needs to be re-combined and re-pushed. The cost of this across an organization as large as Yahoo! is significant.
Serve Files Faster using Combo Handler
Combo Handler, built in collaboration by Yahoo!’s Exceptional Performance team and the groups that support our CDN, is one solution to combine multiple files into a single, larger file.
Combo Handler provides a way to allow developers to maintain the logical organization of their code in separate files, while achieving the advantages of combining those into a single file as part of the final user experience. It alleviates the need for the time-consuming re-build and re-push processes. In addition, Combo Handler integrates seamlessly into a content delivery network, taking full advantage of the benefits of a CDN while reducing the drawbacks of dynamically combining separate files.
We’ve been using this service across many Yahoo! properties for some time now to help improve end users’ response times. Thanks to the YUI team, it is now available to all of you that are using the Yahoo!-hosted YUI JavaScript files. (Note: Combo-handling of CSS files is not supported at this time.) Head over to the YUI Configurator to generate combo-ready filepaths customized for your specific YUI implementation.
Combo Handler Best Practices
When using Combo Handler to combine files, pay special attention to the order in which the files are specified. Not only could there be file dependencies, browsers will only use the cached version of a file if the filename extracted from the URL is identical. For example, suppose the following smaller files (dom.js and event.js) are combined into a single larger file using Combo Handler:
http://yui.yahooapis.com/combo?event.js&dom.js http://yui.yahooapis.com/combo?dom.js&event.js
In the example above, the browser will download and cache both files separately because the filenames are actually different.
Also, you may not always want to combine all files into one single file. Suppose you have one or more scripts that are shared across multiple pages in your site in addition to scripts that are only used on specific pages. By combining everything into one large file and using this file across your entire site, some pages will spend time downloading more than it really needs. Instead, take a look at different types of combinations. You might combine the scripts that are used in every page across your site into one script. Then for each page or group of pages, combine common scripts into another separate script.
Yahoo! HotJobs Combines and Reduces Response Time by 8%!
The Exceptional Performance team ran an experiment with Yahoo! HotJobs to determine the response time savings our users would benefit from by combining multiple files into a single file. Two real user test buckets were created for this experiment. In one bucket, users visited a page with six JavaScript files left uncombined. In the second bucket, users visited the same page with the six JavaScript files combined into one single file.
Combining six JavaScript files into one single JavaScript file improved performance by almost 8% on average for Yahoo! HotJobs’ users on broadband bandwidth speeds and 5% for users on lan. No design or feature changes required!
Keep in mind that the page we tested was already highly optimized for performance and had a YSlow “A” grade. The response time savings depend on a number of factors including number of files combined, browser caching patterns, etc. This experiment supported our previous research, which indicated that reducing HTTP requests is an effective way to improve response times for our end users.
Takeaways
Improve response times by combining multiple JavaScript and CSS files. Yahoo!’s Combo Handler Service is one solution that provides a way to make fewer HTTP requests for Yahoo!-hosted JavaScript files, and also leverages the benefits of a Content Delivery Network.
- Combine scripts and stylesheets to reduce HTTP requests.
- Look at different types of file combinations.
- Avoid users from having to download more than they really need.
- Pay special attention to the order in which files are combined.
Share and extend: Bookmark with Yahoo! My Web | Bookmark with del.icio.us | digg it! | reddit!
Context Menus and Focus in Opera
July 17, 2008 at 4:46 pm by Todd Kloots | In Accessibility, Design, Development | 10 Comments As a JavaScript toolkit developer, there are two features lacking in Opera that have frustrated me for a while: support for the contextmenu DOM event and the ability to override the default rendering of focus via CSS. When Opera released version 9.5, I was disappointed to see that neither of these features were implemented. As frontend engineers, we spend a lot of time responding to decisions made by browser manufacturers, but we don’t get much opportunity to learn the specific thought process behind those decisions. After exchanging some emails with the Opera team, I now have some insight into their decisions and the perspective that perhaps withholding such features could be beneficial to the user.
Background
Although the capabilities of the browser have evolved significantly in recent years, the user’s perception of the browser hasn’t necessarily evolved with it. After the launch of Yahoo! Photos 3.0, I remember a friend of mine emailing me because she was having trouble viewing the large version of her photos. She was repeatedly clicking on each thumbnail without success. Eventually she figured out that she needed to double click on the thumbnails to view the full size image. Double clicking to open a folder or a file is, of course, a natural interaction on the desktop, but for years users were trained not to expect this interaction in the context of web applications.
Some users still don’t expect desktop-like interaction from web applications. I remember logging into my Yahoo! Mail not long ago and seeing checkboxes next to each message. I paused. What were these checkboxes, these artifacts of Web 1.0, doing in my Web 2.0 application? I had been using the new DHTML, Outlook-like version of Yahoo! Mail since its early beta and had become used to dragging and dropping in order to move and delete messages. But the reappearance of these checkboxes was another sign that not every user’s expectations had evolved with the capabilities of the browser.
As the browser has matured it has evolved it into a platform for rich application development, making it possible to deliver applications with a level of interactivity and visual fidelity of those found on the desktop. And while the browser is now a platform, it also continues to play its original Web 1.0 role of an application, a content viewer that enables users to surf all of the news sites, blogs, etc. scattered across the eclectic Web. But as the browser now plays these dual roles of being an application and an application development and delivery platform, how does this duality impact the user in terms of usability and accessibility? And what user-centric features and functionality can consumers expect of a browser, especially one battling with duality? I suspect that Opera’s answer to these questions is that not all users understand the modern browser’s dual role, and that is it necessary to render some fundamental things consistently across experiences within the browser.
Context Menus
Consider context menu functionality. By not implementing the contextmenu event, Opera does not allow frontend engineers to override the
default context menu provided by the browser; all other A-Grade browsers support this feature. What benefit could there be to not implementing the contextmenu event?
If some users perceive everything inside the scope of the browser as a web page, that influences the user’s expectation of what functionality will be surfaced in a context menu. Over the years many users have come to expect that raising a context menu in the scope of a browser will surface browser-centric functionality relative to HTML content (i.e. “Open Link in New Window”), rather than functionality of the web application running within the browser. Therefore, providing a custom context menu for a web application might not be expected or seen as helpful for users who have come to rely on functionality in the browser’s context menu.
The downside is that, by not allowing the developer to provide custom context menu implementations (such as those provided by the YUI Menu Control), Opera is in a small way preventing the user from understanding the browser as a platform for rich application development.
Focus
Focus could be considered as sacred as the context menu. Knowing what element has focus is fundamental to keyboard accessibility. And while most modern browsers support customization of the rendering of focus, is it a good idea to do so? The presentation and behavior is of HTML is now so completely customizable via CSS and JavaScript that the user experience can differ drastically across sites and applications on the web. Keeping something as fundamental as focus consistent means one less thing the user has to re-learn when navigating the across the web. Consider the following example:
Example 1: Anchor Elements (The Good)
| Focused anchor in Opera 9.5 (Mac) | |
| Focused anchor styled as a button in Opera 9.5 (Mac) |
This example illustrates how the focused state of an anchor element is rendered consistently in Opera regardless of how it is styled. This consistency can be considered helpful to the user in that the familiarity of the focus outline conveys the element’s role. Therefore, the user knows what to expect when the element is clicked regardless of how it is styled. However, as illustrated in the following example, this benefit breaks down a little as the focus model for buttons isn’t the same as it is for anchor elements.
Example 2: Buttons (The Bad)
| Focused, unstyled button in Opera 9.5 (Mac) | |
| Focused, styled button in Opera 9.5 (Mac) |
This example illustrates a potential flaw in Opera’s rendering of focus in version 9.5: unlike anchor elements, unstyled and styled buttons get two different renderings of focus, both of which are completely different, and different from the focus style applied to anchor elements. So, in Opera 9.5 there are three different focus implementations for the user to learn: the system default, the Wii-style focus and the dotted border. Compare Opera’s focus implementation to that of Safari or Internet Explorer, where by default focus is rendered consistently across elements of various types.
| Opera | Safari | Description |
|---|---|---|
| Focused, unstyled acnhor | ||
| Focused anchor styled as a button | ||
| Focused, unstyled button | ||
| Focused, styled button |
Since the default implementation of focus can be customized in other browsers, perhaps Opera users still fair better since learning Opera’s three, fixed focus models is ultimately better than having to learn potentially infinitely more. That said, if Opera is going to prevent customization of focus in the interest of usability and accessibility, they could further improve the user experience by providing a consistent implementation of focus across elements. As it stands in Opera 9.5, the following mixed styles can appear together, presenting a confusing set of visual cues:
Example 3: Mixed types together (The Ugly)
| Focused anchor styled as a button in Opera 9.5 (Mac) | |
| Focused, styled button in Opera 9.5 (Mac) |
As illustrated by the first and second examples, Opera has three different, yet fixed implementations of focus. While Opera’s implementation of focus can be considered good insofar as the user only has a limited number of focus models to learn, it might also be considered bad in that it makes it harder to provide a consistent user experience within a single site or web application. For example, if you wanted to place an anchor and button next to each other in a toolbar, but style them consistently so that they both look like buttons, each would still render focus differently in Opera, leaving the user to wonder how the difference is significant.
Conclusion
In Opera designers and developers lose a degree of customization, but the user gains a slightly more consistent browsing experience. In some ways this consistency benefits the user in that fundamental interactions like focus and context menus remain the same regardless of the site or web application in use. However, by limiting certain types of customization designers and developers will find it just a bit harder to provide a consistent user experience within their site or application and to train the user to expect more from Web 2.0.
Share and extend: Bookmark with Yahoo! My Web | Bookmark with del.icio.us | digg it! | reddit!
Combo Handler Service Available for Yahoo-hosted JS
July 16, 2008 at 11:16 am by Eric Miraglia | In Development, Performance | 21 CommentsWe’ve been talking for a long time at Yahoo about the importance of minimizing HTTP requests to improve performance. One important technique for YUI users has long been to use the pre-built "rollup" files (like yahoo-dom-event.js, which combines the YUI Core in a single minified HTTP request) and to create custom rollups that aggregate all of your YUI JS content in a single file. You’ll notice that we do a lot of this on our core Yahoo properties. For example, if you go to check on the Tour de France on Yahoo! Sports, you’ll find that numerous YUI components are aggregated with custom Sports-specific JS resources in a single HTTP request (here’s the aggregate file).
Thanks to the hard work of the Yahoo Exceptional Performance team and the groups that support our CDN, we’re now able to offer ad-hoc file aggregation — "combo handling" — to file served from yui.yahooapis.com. So, a request for the full YUI Rich Text Editor, which previously looked like this…
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/yahoo-dom-event/yahoo-dom-event.js"></script> <script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/container/container_core-min.js"></script> <script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/menu/menu-min.js"></script> <script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/element/element-beta-min.js"></script> <script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/button/button-min.js"></script> <script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/editor/editor-beta-min.js"></script>
…can now be written this way:
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.5.2/build/yahoo-dom-event/yahoo-dom-event.js& 2.5.2/build/container/container_core-min.js&2.5.2/build/menu/menu-min.js& 2.5.2/build/element/element-beta-min.js&2.5.2/build/button/button-min.js& 2.5.2/build/editor/editor-beta-min.js"></script>
In one step, this eliminates five separate HTTP requests.
A few notes regarding combo handling on yui.yahooapis.com:
- If you’re using the YUI Configurator, this option ("Combine All JS Files") is enabled by default as long as you’re using the default base path.
- Combo-handling of YUI CSS files is not supported at this time.
- In an upcoming release, we’ll provide built-in combo-handling support in YUI Loader and restructure filepaths in YUI’s CSS resources to make them combinable as well.
- YUI Configurator will always output the current version of the library, but all YUI JS files from 2.2.0 onward are present on
yui.yahooapis.comand can be combined using the same combo-handling syntax.
We hope combo handling provides a easy performance win for those of you letting Yahoo serve your YUI files. Discussion of combo handling and all YUI issues takes place in our community forum — please join us there and let us know how this works for you.
Share and extend: Bookmark with Yahoo! My Web | Bookmark with del.icio.us | digg it! | reddit!
In the Wild for July 11, 2008
July 11, 2008 at 1:47 pm by Eric Miraglia | In In the Wild | 6 CommentsIn the YUI world of late:
- The guy who started it all: Thomas Sha, the founder of the YUI project and the first frontend engineering manager at Yahoo, was at Adobe at the time of the AIR launch awhile back and they shot some video. If you’re curious to hear from the person who started YUI, here’s a rare chance (while notorious at Yahoo, Thomas does not spend a lot of time on the conference circuit).

- More YUIers in the wild: Andre Charland interviewed YUI CSS guru Nate Koechley for his InsideRIA podcast recently; frequent YUIBlog contributor Christian Heilmann from the Yahoo Developer Network spoke to the folks at The Guardian about his development philosophy, and Inside’s Paul Carvill has a writeup.
Canvas graphing with a touch of YUI: Adam Detrick on 500null has a nice canvas-graphing example that makes use of YUI’s Dom Collection to build a progressively enhanced bar chart using inline tabular data.- Ojay increments: James Coglan has updated his chaining library for YUI, Ojay, to 0.20. Writes James: “We have two new UI packages, Ojay.Overlay and Ojay.Paginator. Overlay gives you a bunch of classes for positioning content on top of the document, producing lightbox effects and the like, and Paginator implements the content slider effect that’s got a lot of attention recently, including the ability to lazy-load pages of content via Ajax, and easy integration with Ojay.History. Both packages come with a collection of events to allow your code to react to changes to the components, just like you would for DOM elements.” Dion has the full story on Ajaxian.
- Mighty parched over here…: Jenny Donnelly has worked hard on DataTable over the past year, and there’s nothing more gratifying to me than to see her work put to good use — in this case, providing us with the ability to sort Irish whiskeys by age (descending, naturally). Now, that’s worth pouring a Dungourney for. Cheers to David and the team.
Maps+ FF Extension: Yahoo Maps is all YUI-based, and so is Rahim Sonawalla’s nifty YUI-based Maps+ Firefox extension. Like others in this category, Map+ allows you to highlight any address in a web page and map the address inline via a context menu.- Dynamic dialogs with Rails and YUI: Eric Hedberg from Enleitened has posted his second in a series of posts about using YUI with Rails. This time he’s taking on dialogs. "In the last tutorial, we embedded a hidden form in a ’show’ view, so we could render a YUI Dialog instead of loading the edit page separately, or using a technique like in-place editing. While this is useful, I find it to be of somewhat limited utility. In this article I’ll demonstrate a technique to move that dialog to the index view using the YUI Connection Manager, so we can skip the show step all together."
- YUI template for Blogger: Will Stranathan posts on his Will at Home blog about implementing a YUI CSS-based Blogger template with full details on how to get this going on your own blog.
- YPulse from Trendics: Kent Johnson from Trendics posted his YUI-based solution for colored pulse alerts, YPulse: “Show a green highlight that fades away on a table row when a new record is added by a user. Add a pulsing yellow glow to a button you want the user to press next. With this open source YPulse library and YUI, both are easy to implement.”
- Website Speed Check: Also from Trendics, the new Website Speed Check tool tests the access time for your website (via a HTTP HEAD request) from locations around the US and reports them in a YUI DataTable. Can’t wait to see some Charts integration here.

Marco van Hylckama Vlieg’s Dark Matter Theme for Pixelpost: Marco writes: "As much as I love Pixelpost, I found that one thing was lacking: a rock solid theme made with web development best practices in mind. I found some decent ones but nothing I really liked enough to put on my own site. Therefore I decided to roll my own and then make it available for all Pixelpost users to use." The result is the gorgeous Dark Matter theme. You can see it in action on Marco’s own photography site.- Asvin Balloo’s JavaScript Text Magnifier: Writes Asvin regarding his magnifier: “The piece of code we will be writing, using the YUI library (my favorite), will allow users to increase/decrease their font size for a block of text, storing the font size in a cookie so that when they return to the page they don’t have to modify the font size again.” While Firefox 3 and Opera are starting to lead the way in full-page zooming, this trick is still a nice one for readers who can benefit from an adjustable font size.
As always, please help us out by letting us know in the comments what we’ve missed out there.
Share and extend: Bookmark with Yahoo! My Web | Bookmark with del.icio.us | digg it! | reddit!

Copyright © 2006-2009 Yahoo! Inc. All rights reserved. Privacy Policy - Terms of Service
Powered by WordPress on Yahoo! Web Hosting.