Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 22, 2015 12:00 pm GMT

How Tabs Should Work

Remy Sharp picks that old chestnut tabs and roasts it afresh on the open fire of JavaScript to see how a fully navigable, accessible and clickable set of tabs can work. Everybody knows some scripting and some CSS can help to make your website bright. Although its been said many times, many ways, please be careful to do it right.


Tabs in browsers (not browser tabs) are one of the oldest custom UI elements in a browser that I can think of. Theyve been done to death. But, sadly, most of the time I come across them, the tabs have been badly, or rather partially, implemented.

So this post is my definition of how a tabbing system should work, and one approach of implementing that.

But tabs are easy, right?

Ive been writing code for tabbing systems in JavaScript for coming up on a decade, and at one point I was pretty proud of how small I could make the JavaScript for the tabbing system:

var tabs = $('.tab').click(function () {  tabs.hide().filter(this.hash).show();}).map(function () {  return $(this.hash)[0];});$('.tab:first').click();

Simple, right? Nearly fits in a tweet (ignoring the whole jQuery library). Still, its riddled with problems that make it a far from perfect solution.

Requirements: what makes the perfect tab?

  1. All content is navigable and available without JavaScript (crawler-compatible and low JS-compatible).
  2. ARIA roles.
  3. The tabs are anchor links that:
    • are clickable
    • have block layout
    • have their href pointing to the id of the panel element
    • use the correct cursor (i.e. cursor: pointer).
  4. Since tabs are clickable, the user can open in a new tab/window and the page correctly loads with the correct tab open.
  5. Right-clicking (and Shift-clicking) doesnt cause the tab to be selected.
  6. Native browser Back/Forward button correctly changes the state of the selected tab (think about it working exactly as if there were no JavaScript in place).

The first three points are all to do with the semantics of the markup and how the markup has been styled. I think its easy to do a good job by thinking of tabs as links, and not as some part of an application. Links are navigable, and they should work the same way other links on the page work.

The last three points are JavaScript problems. Lets investigate that.

The shitmus test

Like a litmus test, heres a couple of quick ways you can tell if a tabbing system is poorly implemented:

  • Change tab, then use the Back button (or keyboard shortcut) and it breaks
  • The tab isnt a link, so you cant open it in a new tab

These two basic things are, to me, the bare minimum that a tabbing system should have.

Why is this important?

The people who push their so-called native apps on users cant have more reasons why the web sucks. If something as basic as a tab doesnt work, obviously theres more ammo to push a closed native app or platform on your users.

If youre going to be a web developer, one of your responsibilities is to maintain established interactivity paradigms. This doesnt mean dont innovate. But it does mean: stop fucking up my scrolling experience with your poorly executed scroll effects. </rant> :breath:

URI fragment, absolute URL or query string?

A URI fragment (AKA the # hash bit) would be using mysite.com/config#content to show the content panel. A fully addressable URL would be mysite.com/config/content. Using a query string (by way of filtering the page): mysite.com/config?tab=content.

This decision really depends on the context of your tabbing system. FOr something like GitHubs tabs to view a pull request, it makes sense that the full URL changes.

For our problem though, I want to solve the issue when the page doesnt do a full URL update; that is, your regular run-of-the-mill tabbing system.

I used to be from the school of using the hash to show the correct tab, but Ive recently been exploring whether the query string can be used. The biggest reason is that multiple hashes dont work, and comma-separated hash fragments dont make any sense to control multiple tabs (since it doesnt actually link to anything).

For this article, Ill keep focused on using a single tabbing system and a hash on the URL to control the tabs.

Markup

Im going to assume subcontent, so my markup would look like this (yes, this is a cat demo):

<ul class="tabs">  <li><a class="tab" href="#dizzy">Dizzy</a></li>  <li><a class="tab" href="#ninja">Ninja</a></li>  <li><a class="tab" href="#missy">Missy</a></li></ul><div id="dizzy">  <!-- panel content --></div><div id="ninja">  <!-- panel content --></div><div id="missy">  <!-- panel content --></div>

Its important to note that in the markup the link used for an individual tab references its panel content using the hash, pointing to the id on the panel. This will allow our content to connect up without JavaScript and give us a bunch of features for free, which well see once were on to writing the code.

URL-driven tabbing systems

Instead of making the code responsive to the users input, were going to exclusively use the browser URL and the hashchange event on the window to drive this tabbing system. This way we get Back button support for free.

With that in mind, lets start building up our code. Ill assume we have the jQuery library, but Ive also provided the full code working without a library (vanilla, if you will), but it depends on relatively new (polyfillable) tech like classList and dataset (which generally have IE10 and all other browser support).

Note that Ill start with the simplest solution, and Ill refactor the code as I go along, like in places where I keep calling jQuery selectors.

function show(id) {  // remove the selected class from the tabs,  // and add it back to the one the user selected  $('.tab').removeClass('selected').filter(function () {    return (this.hash === id);  }).addClass('selected');  // now hide all the panels, then filter to  // the one we're interested in, and show it  $('.panel').hide().filter(id).show();}$(window).on('hashchange', function () {  show(location.hash);});// initialise by showing the first panelshow('#dizzy');

This works pretty well for such little code. Notice that we dont have any click handlers for the user and the Back button works right out of the box.

However, theres a number of problems we need to fix:

  1. The initialised tab is hard-coded to the first panel, rather than whats on the URL.
  2. If theres no hash on the URL, all the panels are hidden (and thus broken).
  3. If you scroll to the bottom of the example, youll find a top link; clicking that will break our tabbing system.
  4. Ive purposely made the page long, so that when you click on a tab, youll see the page scrolls to the top of the tab. Not a huge deal, but a bit annoying.

From our criteria at the start of this post, weve already solved items 4 and 5. Not a terrible start. Lets solve items 1 through 3 next.

Using the URL to initialise correctly and protect from breakage

Instead of arbitrarily picking the first panel from our collection, the code should read the current location.hash and use that if its available.

The problem is: what if the hash on the URL isnt actually for a tab?

The solution here is that we need to cache a list of known panel IDs. In fact, well-written DOM scripting wont continuously search the DOM for nodes. That is, when the show function kept calling $('.tab').each(...) it was wasteful. The result of $('.tab') should be cached.

So now the code will collect all the tabs, then find the related panels from those tabs, and well use that list to double the values we give the show function (during initialisation, for instance).

// collect all the tabsvar tabs = $('.tab');// get an array of the panel ids (from the anchor hash)var targets = tabs.map(function () {  return this.hash;}).get();// use those ids to get a jQuery collection of panelsvar panels = $(targets.join(','));function show(id) {  // if no value was given, let's take the first panel  if (!id) {    id = targets[0];  }  // remove the selected class from the tabs,  // and add it back to the one the user selected  tabs.removeClass('selected').filter(function () {    return (this.hash === id);  }).addClass('selected');  // now hide all the panels, then filter to  // the one we're interested in, and show it  panels.hide().filter(id).show();}$(window).on('hashchange', function () {  var hash = location.hash;  if (targets.indexOf(hash) !== -1) {    show(hash);  }});// initialiseshow(targets.indexOf(location.hash) !== -1 ? location.hash : '');

The core of working out which tab to initialise with is solved in that last line: is there a location.hash? Is it in our list of valid targets (panels)? If so, select that tab.

The second breakage we saw in the original demo was that clicking the top link would break our tabs. This was due to the hashchange event firing and the code didnt validate the hash that was passed. Now this happens, the panels dont break.

So far weve got a tabbing system that:

  • Works without JavaScript.
  • Supports right-click and Shift-click (and doesnt select in these cases).
  • Loads the correct panel if you start with a hash.
  • Supports native browser navigation.
  • Supports the keyboard.

The only annoying problem we have now is that the page jumps when a tab is selected. Thats due to the browser following the default behaviour of an internal link on the page. To solve this, things are going to get a little hairy, but its all for a good cause.

Removing the jump to tab

Youd be forgiven for thinking you just need to hook a click handler and return false. Its what I started with. Only thats not the solution. If we add the click handler, it breaks all the right-click and Shift-click support.

There may be another way to solve this, but what follows is the way I found and it works. Its just a bit hairy, as I said.

Were going to strip the id attribute off the target panel when the user tries to navigate to it, and then put it back on once the show code starts to run. This change will mean the browser has nowhere to navigate to for that moment, and wont jump the page.

The change involves the following:

  1. Add a click handle that removes the id from the target panel, and cache this in a target variable that well use later in hashchange (see point 4).
  2. In the same click handler, set the location.hash to the current links hash. This is important because it forces a hashchange event regardless of whether the URL actually changed, which prevents the tabs breaking (try it yourself by removing this line).
  3. For each panel, put a backup copy of the id attribute in a data property (Ive called it old-id).
  4. When the hashchange event fires, if we have a target value, lets put the id back on the panel.

These changes result in this final code:

/*global $*/// a temp value to cache *what* we're about to showvar target = null;// collect all the tabsvar tabs = $('.tab').on('click', function () {  target = $(this.hash).removeAttr('id');  // if the URL isn't going to change, then hashchange  // event doesn't fire, so we trigger the update manually  if (location.hash === this.hash) {    // but this has to happen after the DOM update has    // completed, so we wrap it in a setTimeout 0    setTimeout(update, 0);  }});// get an array of the panel ids (from the anchor hash)var targets = tabs.map(function () {  return this.hash;}).get();// use those ids to get a jQuery collection of panelsvar panels = $(targets.join(',')).each(function () {  // keep a copy of what the original el.id was  $(this).data('old-id', this.id);});function update() {  if (target) {    target.attr('id', target.data('old-id'));    target = null;  }  var hash = window.location.hash;  if (targets.indexOf(hash) !== -1) {    show(hash);  }}function show(id) {  // if no value was given, let's take the first panel  if (!id) {    id = targets[0];  }  // remove the selected class from the tabs,  // and add it back to the one the user selected  tabs.removeClass('selected').filter(function () {    return (this.hash === id);  }).addClass('selected');  // now hide all the panels, then filter to  // the one we're interested in, and show it  panels.hide().filter(id).show();}$(window).on('hashchange', update);// initialiseif (targets.indexOf(window.location.hash) !== -1) {  update();} else {  show();}

This version now meets all the criteria I mentioned in my original list, except for the ARIA roles and accessibility. Getting this support is actually very cheap to add.

ARIA roles

This article on ARIA tabs made it very easy to get the tabbing system working as I wanted.

The tasks were simple:

  1. Add aria-role set to tab for the tabs, and panel for the panels.
  2. Set aria-controls on the tabs to point to their related panel (by id).
  3. I use JavaScript to add tabindex=0 to all the tab elements.
  4. When I add the selected class to the tab, I also set aria-selected to true and, inversely, when I remove the selected class I set aria-selected to false.
  5. When I hide the panels I add aria-hidden=true, and when I show the specific panel I set aria-hidden=false.

And thats it. Very small changes to get full sign-off that the tabbing system is bulletproof and accessible.

Check out the final version (and the non-jQuery version as promised).

In conclusion

Theres a lot of tab implementations out there, but theres an equal amount that break the browsing paradigm and the simple linkability of content. Clearly theres a special hell for those tab systems that dont even use links, but I think its clear that even in something thats relatively simple, its the small details that make or break the user experience.

Obviously there are corners Ive not explored, like when theres more than one set of tabs on a page, and equally whether you should deliver the initial markup with the correct tab selected. I think the answer lies in using query strings in combination with hashes on the URL, but maybe thats for another year!


About the author

Remy Sharp is the founder and curator of Full Frontal, the UK based JavaScript conference. He also ran jQuery for Designers, co-authored Introducing HTML5 (adding all the JavaScripty bits) and likes to grumble on Twitter.

Whilst he’s not writing articles or running and speaking at conferences, he runs his own development and training company in Brighton called Left Logic. And he built these too: Confwall, jsbin.com, html5demos.com, remote-tilt.com, responsivepx.com, nodemon, inliner, mit-license.org, snapbird.org, 5 minute fork and jsconsole.com!

More articles by Remy


Original Link: http://feedproxy.google.com/~r/24ways/~3/3C2Ut1-UBVM/

Share this article:    Share on Facebook
View Full Article

24 Ways

# 24 ways is an edgeofmyseat.com production. # Edited by Drew McLellan and Brian Suda. # Assisted by Anna Debenham and Owen Gregory.

More About this Source Visit 24 Ways