del.icio.us .:. tweet

Toggling Element Visibility .:. kentbrewster.com

I've transplanted Toggling Element Visiblity from its old home on the Mindsack; kindly update your bookmarks.

What's This?
  • Modern sites will often include on-page interactions with headlines leading to hidden content. Here's an unobtrusive, semantically valid way of presenting content that will degrade nicely for non-CSS-aware browsers.
Things to Do and Notice
  • Click the text inside any header block and the content should toggle. (The entire area is hot for Gecko browsers; not so much for IE.) If the content underneath is visible, it should go away. If it's gone, it should come back.
  • The state indicator (the "norgie," which rhymes with "corgi," not "orgy") should flip, to show that something's changed.
How It Works

Please do read the source; it's all obsessively commented and ought to be pretty self-explanatory. Here are some of the highlights:

  • After the page loads completely, we crawl through the entire document tree and look for block-level elements styled with class name toggle. If we find one that says toggle closed, we immediately hide its next sibling element, by styling it with class name hidden.
  • When we find one, we tell it to listen for mouse clicks.
  • When one of our pet elements hears a click, it leaps into action, hiding (or showing) its next available sibling, the same way we did it during the initial crawl.
  • All three class names (toggle, closed, and hidden) are fed in at the bottom in the init call, and may be changed to any valid class name.

That's it! With only a single global variable we're making a good-faith attempt to stay out of the general namespace, and we're not capturing any far-reaching events like document.onmousedown so it ought to work and play nicely with other scripts. Naturally you should feel free to change my KENTBREW.toggle object namespace to whatever you like.

Caveats and Gotchas
  • Nope, sorry, this is not going to work on non-CSS-aware browsers. They will see everything ... but if you're using semantically valid structure, that should not be a problem.
  • Yes, we're using JavaScript to affect the presentation layer rather than the interaction layer. Until we get adjacent-sibling selectors working on all major browsers, we're stuck with that initial hide-all-the-hidden-stuff loop. If you're unhappy with this, simply style all of the elements that should be hidden on page load with class name "hidden," and the initial loop through will have no effect.
  • It's not as accessible as it ought to be; if you don't have a mouse, you're not going to be able to open and close these divisions. For that you'd want to style a proper link, and toggle the parent's next sibling.
The HTML
<html>
<head>
<title>My Toggled Page</title>
<link rel="stylesheet" type="text/css" href="toggle.css" />
</head>
<body>
<dl>
   <dt class="toggle">This is an open headline.</dt>
   <dd>This will hide if the headline above is toggled.</dd>
   <dt class="toggle closed">This is a closed headline</dt>
   <dd>This will show if the headline above is toggled.</dd>
</dl>
<script type="text/javascript" src="toggle.js"></script>
</body>
</html>
The CSS
.toggle {
  background: transparent url('norgie_open.gif')  .25em .25em no-repeat;
  text-indent: 20px;
  cursor:pointer;
}
.toggle.closed {
  background: transparent url('norgie_closed.gif') .25em .25em no-repeat;
}
.hidden {
  display:none;
}
The JavaScript
// toggling event visibility
// v5 Kent Brewster, 7/11/2006
// questions? comments? dirty jokes?
// please leave 'em here:
// http://kentbrewster.com/toggle
// feel free to use or abuse this code
// but please leave this notice intact

// namespace protection: one global variable to rule them all

var KENTBREW = window.KENTBREW || {};

KENTBREW.toggle = function() {

   // private bucket for scope-sensitive variables -- thanks, Dustin
   var $ = {};
   
   return {   
      init : function(selfObj, toggleClass, toggleClosed, toggleHidden) {
         // first, a brute-force hack to decode the calling function's 
         // name and store it upstairs, safe from scope creep
         $.selfName = this.getSelfName(selfObj);
         
         // we're going to hang all variables that might be confused 
         // by scope changes later onto KENTBREW.variables, which is aliased to $.  
         // In this case it's three class names, fed in from the init call:

         $.toggleClass = toggleClass;
         $.toggleClosed = toggleClosed;
         $.toggleHidden = toggleHidden;
         
         // crawl through the document, look for toggled elements
         this.crawl(document.body);
      },
      crawl : function(el) {
         // get this element's next sibling
         var nextSib = this.getNextSibling(el);

         // if it has a class name, the class name matches our toggle class, and there's something there to toggle:
         if (el.className && el.className.match($.toggleClass) && nextSib)
         {
            // to avoid scope loss, attach onmouseup to the toggle function with eval and $.selfName
            el.onmouseup = function () { 
               eval($.selfName + '.toggleState(this)'); 
            };
            
            // if the next sib ought to be hidden and it isn't already, hide it
            if (el.className.match($.toggleClosed) && nextSib && !nextSib.className.match($.toggleHidden)) {
               nextSib.className += ' ' + $.toggleHidden;
            }
         }

         // is there more to do? Do it, if so:
         if (el.firstChild) {
            this.crawl(el.firstChild);
         }
         
         if (nextSib) {
            this.crawl(nextSib);
         }
      },
      toggleState : function(el) {
         // change the style of the triggering element
         if(el.className.match($.toggleClosed)) {
            el.className = el.className.replace($.toggleClosed, '');
         }
         else {
            el.className = el.className + ' ' + $.toggleClosed;
         }

         // the norgie we clicked has changed.  Now we need to
         // change the style of its parent node's next sibling
         var nextSib = this.getNextSibling(el);

         // check if it's really there; other scripts could have removed it
         if(nextSib && nextSib.className.match($.toggleHidden)) {
            nextSib.className = nextSib.className.replace($.toggleHidden, '');
         }
         else {
            nextSib.className += ' ' + $.toggleHidden;
         }
      },
      getNextSibling : function(el) {
         var nextSib = el.nextSibling;
         // hack for Gecko browsers
         if (nextSib && nextSib.nodeType != 1) {
            nextSib = nextSib.nextSibling;
         }
         return nextSib;
      },
      getSelfName : function(selfObj) {
         // icky hack to get contents of selfObj into a string
         // suggestions will be gratefully appreciated
         var s = document.createElement('SPAN');
         s.innerHTML = selfObj;
         // cut the fat, split the meat to namespace array
         var nameSpace = s.innerHTML.split('{')[1].split('(')[0].replace(/^\s+/, '').split('.');
         var selfName = '';
         // here we assume that the main function is up one level from the init function
         for (var i = 0; i < nameSpace.length - 1; i++) {
            if (selfName) {
               selfName += '.';
            }
            selfName += nameSpace[i];
         }
         return selfName;
      }
   };
}();

// feed it the CSS class names of your choice
window.onload = function() { 
   KENTBREW.toggle.init(arguments.callee, 'toggle', 'closed', 'hidden');
}();
Directions for Future Development
  • Multiple levels (a tree-shaped menu, for instance) are definitely possible. I've chosen to show this in the context of a definition list, but it'd be perfectly fine to toggle list items inside of other list items, and create a tree-shaped menu. If you toggled open a submenu and then closed its parent, the submenu would still be open when you opened its parent back up again.
Acknowledgements
Some Rights Reserved
  • Feel free to grab, modify, and use the HTML, Javascript, and CSS anywhere you like, but:
  • Please credit the work to me, include a link back here, and post a comment letting me know where to find it. Painless, eh?

Comments from before Disqus:

Terry Riegel .:. 2010-07-15 07:34:00
Ahh, I see you are using the newer style event handlers for your live code as well.
Terry Riegel .:. 2010-07-15 07:31:04
Kent,

I just had a need to toggle and immediatly thought of this post. So I shamelessly cut and pasted into my site, but had a little trouble with your code. For some reason not exactly clear to me the document.body was null using your event handler. So I added the event handler I use (modified slightly from Resig), and it seems to suit my needs.

Thanks for the resource.

Terry Riegel

observe: function( obj, type, fn ) {
if (!(obj['observing'+type])){
if ( obj.attachEvent ) {
obj['e'+type+fn] = fn;
obj[type+fn] = function(){obj['e'+type+fn]( window.event );};
obj.attachEvent( 'on'+type, obj[type+fn] );
} else {
obj.addEventListener( type, fn, false );
}
obj['observing'+type]=true;
}
}

// feed it the CSS class names of your choice
KENTBREW.toggle.observe(window,'load',function() {KENTBREW.toggle.init(arguments.callee, 'toggle', 'closed', 'hidden');});
Kent Brewster .:. 2009-09-19 08:39:50
Hi, Twitter friends. Note: although sound, this technique is very old and needs to be refactored.
David Hewitt .:. 2008-12-04 18:34:49
Hi,
I have to say that i am very fond of this technique and i love to employ it myself.
However i am not a coder as such and would very much like to see an adaptation of this method that automatically hides all OTHER "Drop Downs" when ever a "Drop Down" is activated!
I would prefer this method for tidyness and also for the fact that i believe it is more intuitive as these drop downs are like "Draws" in a filing cabinet and who opens all their draws at once? No one....
If you can help i belive this would jazz it up a bit...also as an added nicety i have included (in my own script)an adaptation/add on to this basic idea that does inhance it visually somewhat and that is to combine it with a mouse flyover effect to switch your "handle icon" (to an animated gif) which is a nice touch. The code i used for this rollover I got from here:
"http://scriptasylum.com/tutorials/rollovers/rollovers.html" this code is the best as it does not require seperate code in the script for each instance....
Also i find it is cool to use A DSS Style from your tag that encloses the text and the icon so that placing your mouse over either the text or the icon causes the icon animation to proceed as well as changing the text color of the text like a link(also having the text underlined too)... both these devices i think are logical as clicking on a heading/icon to drop down a table is (in the mind of the user) the same as clicking on a link...
And having the text highlight on flyover as well as the animation also serves to remind the user that there is something that can be clicked on...which then greatly lessens the problem of people not realizing the hidden content.
Jeroen Lejeune .:. 2008-04-05 10:07:14
I get it now, thanks :)
Kent Brewster .:. 2008-04-03 07:43:38
Right here:

if(el.className.match($.toggleClosed)) {
el.className = el.className.replace($.toggleClosed, '');
} else {
el.className = el.className + ' ' + $.toggleClosed;
}

We're saying "If you see the closed toggle class, remove it. If you don't, add it." Make sense?
Jeroen Lejeune .:. 2008-04-02 14:34:18
I'm having difficulty understanding some tags:
In my stylesheet it says ".toggle.closed" (class toggle class closed?)
Yet in my HTML I get class="toggle closed" (no period in between)
at the very end of the javascript it says " 'toggle', 'closed', 'hidden');"

I'm having trouble understanding the relationship between these. I don't know any Javascript, but I have a firm knowledge of css and html.

What I'm trying to do is use this script for both my news and my navigation menu, but since I don't fully understand what's going on, I'm having a hard time doing so.

Where in your Javascript do you say (in human language) "Dear script, please change the class "toggle" to "toggle closed""?
Kent Brewster .:. 2008-03-17 08:48:24
Right, this is due for a merciless refactoring, and some event delegation. In the meantime, here's a hint: you should probably pick a single tag such as A, DT, or H3 as your toggle handle, get all of them into an array:

var myTag = 'A';
var thePossibles = document.getElementsByTagName(myTag);

Loop through thePossibles, check for the class name, and apply the toggle logic--the part in function crawl(), where we match the className--if it appears.
Carlton .:. 2008-03-17 02:44:44
Nice library, tried an example and worked really well but then tried to embed into quite a large page with quite a few elements on it and got the following error...

too much recursion
var nextSib = this.getNextSibling(el);
Kent Brewster .:. 2008-01-31 11:11:57
You're missing a closing curly bracket on the very bottom line of toggle.js.
adam .:. 2008-01-31 10:48:54
for some reason, the toggle code does not work on my site located here. Any thoughts? thank you

http://www.shopmicrofiber.com/what-is-Microfiber.asp

Adam

Copyright Kent Brewster 1987-2014 .:. FAQ .:. RSS .:. Contact