Managing Style and State

Even on the simplest of web pages, we have to handle different states that parts of our document may be in at any time. Probably the most common of which is the simple rollover. Move your mouse over an element and the image changes. This was traditionally done with images and JavaScript until CSS came along and gave us the :hover pseudo class.

What happens when a state needs a little more permanency than a simple rollover? For example, let's say you wish to show which page is currently selected within your navigation. Here's some sample navigation:

<ul>
   <li><a href="#">Home</a></li>
   <li><a href="#">About</a></li>
   <li><a href="#">Contact</a></li>
</ul>

To show which page is currently selected, we might add a selected class on the element — either by hand on each page or programmatically using a server-side language:

<ul>
   <li class="home"><a href="#">Home</a></li>
   <li class="about selected"><a href="#">About</a></li>
   <li class="contact"><a href="#">Contact</a></li>
</ul>

<style type="text/css">
   .selected {border:1px solid red;}
</style>

A technique seen more often these days (and one that I use myself) is to set the current page on a parent element such as the UL or — more likely — the BODY.

<body class="about">
<ul>
   <li class="home"><a href="#">Home</a></li>
   <li class="about"><a href="#">About</a></li>
   <li class="contact"><a href="#">Contact</a></li>
</ul>
</body>

<style type="text/css">
   body.about .about {border:1px solid red;}
</style>

This works well for a couple reasons:

  1. Programmatically, it's easier to output a page variable in one place than having to loop through a block of navigation trying to determine the current element.
  2. With the property set in a more global location, we have a hook to do other page-specific styles that apply elsewhere on the page.

Enter JavaScript

As you've just seen, setting state when the page load is fairly straightforward and of great benefit but what if you need to change page state on the fly? It might be a tab pane or some other show/hide technique.

The initial approach that comes to mind is to manipulate the styles directly:

var el = document.getElementById('myElement');
el.style.display = (el.style.display == 'none') ? '' : 'none';

However, with the tab pane, you start having to change properties like which is the active pane, which is hidden and storing which is the active state. Here's some snippets from that tab pane example to demonstrate what I mean:

if (this.selectedIndex != n) {
   if (this.selectedIndex != null && this.pages[ this.selectedIndex ] != null )
   this.pages[ this.selectedIndex ].hide();
   this.selectedIndex = n;
   this.pages[ this.selectedIndex ].show();

In this snippet, it uses an internal property, selectedIndex to remember which is the current element. If somebody clicks to select a tab, it checks to see if it's different than the current one and if so, hides it. It stores the new tab as the selectedIndex and then proceeds to show it.

var el = this.tab;
var s = el.className + " selected";
s = s.replace(/ +/g, " ");
el.className = s;

this.element.style.display = "block";

This is how an element is shown. First, the tab is set to selected using a class name and then the element which contains the content has its style set to block.

Now, to clarify, I'm not picking on Erik Arvidsson's work. It works really well and there's nothing wrong with the code. But could it be made simpler, especially when you know the HTML structure ahead of time?

Getting some class

Let's look at a code outline to see how our tab panel might be structured:

<div id="tabContainer">
   <div id="about" class="pane">
      <h2>Pane 1</h2>
      <div class="contents">This is pane 1</div>
   </div>

   <div id="contact" class="pane">
      <h2>Pane 2</h2>
      <div class="contents"></div>
   </div>

   <div id="help" class="pane">
      <h2>Pane 3</h2>
      <div class="contents"></div>
   </div>
</div>

With this basic structure, we have a container with three possible panes. Now, to set which is the active pane, we simply set a class name on the container and use the stylesheet to control the look and feel of the rest of the tabs.

function setactive()
{
  var el = this;
  var className = el.parentNode.id;
  el.parentNode.parentNode.className = className;
}

window.onload = function()
{
  var titles = document.getElementsByTagName('h2');
  for(var i=0;i<titles.length;i++)
  {
    titles[i].onclick = setactive;
  }
}

The onload event attaches the click handler to each of the headers to run the setactive function. All the function does is take the clicked element, grabs the id from the parent element (the pane), and attaches it to the grandparent element (the container). With the active pane now declared, we simply use CSS to define the look and feel:

#tabContainer.about #about .contents,
#tabContainer.contact #contact .contents,
#tabContainer.help #help .contents
  {display:block;}

#tabContainer.about #about h2,
#tabContainer.contact #contact h2,
#tabContainer.help #help h2
  {border:1px solid red;border-width:1px 1px 0;}

The first block "turns on" the contents and the second block demonstrates what an active tab should look like.

Check out the final demonstration to see this in action.

Things to remember

Apply the class name on the most relevant parent element. Adding everything to the body element could get confusing quickly as you try to keep competing element and class names from conflicting.

Be sure to consider how things will look and behave without JavaScript or without CSS. In my example, if a user did not have JavaScript, the content in any of the panels would not be visible. There are a couple techniques available to do this. You could add an isjs class name to the document via JavaScript and have your styles use it as the trigger. Alternatively, you could use JavaScript to attach a new style sheet that styled anything that was JavaScript dependant.

Consider accessibility. Try accessing the panels using the keyboard. To get around this, you can set a tab index on the headers and use onfocus instead of onclick. Here's a more accessible version. I've set the tabindex to 0 allowing the headers to receive focus by tabbing through them. The headers also receive focus when I click on them with the mouse, removing the need to use an onclick event at all.

Published May 03, 2007
Categorized as JavaScript
Short URL: https://snook.ca/s/801

Conversation

13 Comments · RSS feed
Fredrik Wärnsberg said on May 03, 2007

If I recall correctly, Jeremy Keith wrote in his book "DOM Scripting" that most screen readers and such equipment treats onclick as onfocus thus making onfocus superfluous to use?

Neat little trick though!

Jonathan Snook said on May 03, 2007

@Fredrik: as far as I know, that only applies to elements that normally have "clickability" like links and buttons. Because I've attached behaviour to an element, I have to A) make it focusable by giving it a tabindex and B) use onfocus to react to the focus.

kayloe said on May 03, 2007

this is a really neat script combo. well done (yet again) jonathan.

James Oppenheim said on May 04, 2007

Nice clean JavaScript. I am sure I have written the exact same code, only it was much more verbose. Well done once again.

Patrick Fitzgerald said on May 04, 2007

Another advantage of using classes instead of directly setting the styles: you can use different styles for printing the page.

So when you print your tabbed interface, instead of just seeing the selected tab, you could print all of the tabs.

I use a similar technique in my tabber script:
http://www.barelyfitz.com/projects/tabber/

Akotan said on May 04, 2007

I wanted to know this: is there a way to do a drop-down menu working with javascript casting actions on the fly, on items that need action?

I made something like this:

<ul>
 <li><a href="#">My Menu</a>
  <ul>
   <li><a href="home.htm">Home</a></li>
   <li><a href="about.htm">About</a></li>
  </ul>
 <li><a href="home.htm">Home</a></li>
</ul>

Without so many classes and ids...

And wrote an javascript code that identifies the former <li>, that instruted by CSS hides the <ul> nestled, and when I click the <a> link, set the <ul> display to "block".

And have some tons of decorations, backgrounds setted by CSS and IE6 on my way... And here lies my question: I don't have a clue if what I wrote is right and if there are "the" way of doing this right?

Is there a better way to force IE6 behave? Or should I give up?

Webice said on May 06, 2007

Very clean implementation thanks for sharing.

Mike said on May 06, 2007

I don't like to include a link to the current page. I prefer this structure:

<ul>
<li><a href="#">Home</a></li>
<li class="active">About</li>
<li><a href="#">Contact</a></li>
</ul>

Daniel said on May 11, 2007

Amazing ... i really love the simple tips in life !

kos said on May 11, 2007

But Mike

  • About
  • it's not a link. I prefer the akotan structure where is drop-down menu...

    Thierry said on May 22, 2007

    I like the idea of using "tabindex", but is it kosher?

    Also, I think it would be great to provide a mechanism that would allow keyboard users to jump from one heading to another, skipping all elements in a panel (links, buttons, etc.).

    Ronhead said on October 12, 2007

    hi everybody ive a big problem whit this thing, ive this msge:
    this.pages[this.selectedIndex] has no properties

    i cant understand the problem if u think any please help me
    ...

    the code:

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
    <html><!-- InstanceBegin template="/Templates/fullbody.dwt" codeOutsideHTMLIsLocked="false" -->
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
    <!-- InstanceBeginEditable name="doctitle" -->
    <title>Untitled Document</title>
    <script type="text/javascript" src="popup.js"></script>
    <!-- InstanceEndEditable -->

    <!-- InstanceBeginEditable name="head" --><!-- InstanceEndEditable -->

    <link href="css/estilos.css" rel="stylesheet" type="text/css" />

    </head>

    <body>
    <link href="css/print.css" rel="stylesheet" >
    <link href="css/estilos.css" rel="stylesheet" >

    <link id="luna-tab-style-sheet" type="text/css" rel="stylesheet" href="js/tabs/tabpane.css" />
    <script type="text/javascript" src="js/tabs/tabpane.js"></script>
    <table width="95%" align="center" border="0">
    <tr>
    <td><div class="tab-page" id="modules-cpanel">
    <script type="text/javascript">
    var tabPane1 = new WebFXTabPane( document.getElementById( "modules-cpanel" ), 0 )
    <!-- ================= PROBLEM ==================== -->
    tabPane1.setSelectedIndex(1);

    </script>
    <div class="tab-page" id="modulo1">
    <h2 class="tab">General</h2>
    <div align="left"> Complete datos</div>







    </div>
    <div class="tab-page" id="modulo2">
    <h2 class="tab">Tipologías</h2>
    <div align="left"> Agregar Tipología de Producto </div>






    </div>
    <div class="tab-page" id="modulo3">
    <h2 class="tab">Clases Pedido </h2>
    <div align="left"> Agregar Clase de Pedido</div>














    </div>
    <div class="tab-page" id="modulo3">
    <h2 class="tab">Priorización</h2>
    <div align="left"> Orden de Criterios de Priorización </div>










    </div>
    </td>
    </tr>
    </table>

     

    <!-- InstanceEndEditable --></td>
    </tr>
    </table>
    </td>
    </tr>

    <!-- InstanceEnd -->

    overeedow said on February 07, 2011

    I am 31 years old and live with excruciating pain every day of my life for over 5 years now. That's what i want to say here.

    Sorry, comments are closed for this post. If you have any further questions or comments, feel free to send them to me directly.