JavaScript Accordion using Prototype

In my search for an accordion solution I came across two of mention. The first was from stickmanlabs. It had good functionality and the ability to create a horizontal accordion. But support for IE6 was lacking (although damn IE6) and and it used an older version of Prototype. I kept looking and came across the second, by Brian Crescimanno. This was a little smoother, with leaner markup. The code was cleaner and appeared very similar to stickmanlabs. So, I felt like I was on the right track. After reviewing the demo and reading through the comments, it needed more. So, in typical developer fashion, I made my own.

The Merge

In this case, I felt there were pieces of both solutions that were good. I decided to merge the two, and use Brian Crescimanno’s as a base. If you want more detail on the individual solutions, I suggest reviewing the articles above. As such, I have provided an outline of the changes:

  • Updated stickman’s horizontal functionality to Prototype 1.6.
  • Made more prototype-ish. Code wasn’t taking full advantage of Prototype.
  • Refactored methods by grouping similar actions (e.g. toggle/clickHandler)
  • Removed modification of display styles, used height/width consistently.
  • Converted initialize parameter into an options hash.
  • Ability to change the "toggle" event.
  • Ability for multiple nodes to be expanded in a vertical accordion. Neither solution had this, and in my opinion it mimics a true accordion.
  • Better degradation. Modified "toggle" elements to be anchor tags. Integrated CSS styles to allow proper display if JavaScript is disabled.

The Solution

So without farther ado, here is an example of the merged accordion solution or you can download the source.

The markup requires three placeholders. A containing element with an id attribute you reference with JavaScript. A toggle element with a class attribute of accordion-toggle. I use an anchor tag for semantic reasons, but it can be anything. A content element with a class attribute of accordion-content. There is a one to one relationship between toggle and content elements.

<div id="test-accordion">
    <a href="#" class="accordion-toggle">Main</a>
    <div class="accordion-content">
        <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
        <p>Mauris dictum congue lectus.</p>
    </div>

Currently the configuration options are the accordion type and event. Type can be horizontal, vertical (default), or vertical-multiple. Event can be any event supported by Prototype (e.g. mouseover, click). In addition, you can change the class names within the initialize method. However, keep in mind these are shared for all your accordions. The following code from the example creates a vertical and horizontal accordion on page load:

document.observe("dom:loaded", function() {
    accordion = new Accordion({id: "test-accordion"});
    accordion2 = new Accordion({id: "test2-accordion", type: 'horizontal'});
});

The CSS is the trickest part. In modifying the CSS, I found that most bugs were related to the styles. When styling your accordion remember the box model. Padding, margin, and borders all affect the effect. Which makes sense considering this solution modifies height/width. If you start noticing jumpiness in the effect, check these properties.

In Closing

An accordion solution is relatively progressive. Although I feel this solution degrades better than others, it is not fully functional without JavaScript enabled. To resolve this, you will need some back-end support. By modifying the toggle elements to link to the current page with a URL parameter. On page load the back-end could parse this URL parameter to identify which node to expand and add the necessary CSS class (active).

A Disclaiming Note

Understand the web is a continually evolving environment. The code within this article is offered with an as-is warranty. My goal is that the article may help more than the code. Nonetheless, I always welcome your feedback, good and bad. Just know, that I know, this is not the solution and therefore may not work for you… Although in an ideal development world it would.

18 thoughts on “JavaScript Accordion using Prototype

  1. I like your accordion a lot. Clean code and easy to use, but I have a problem with a checkbox inside the div with class accordion-content. It’s not clickable.

  2. Chrisiaan,

    Initially the click event was observed for the entire accordion. When reviewing the other two solutions I thought this was interesting, but didn’t change it. Your checkbox example proved this to be a major bug, as it was intercepting the click event.

    I modified the code so the click event is only observed for the toggle elements. As it should be ;)

  3. Hi,

    I have extended your Accordion a bit. There is a new callback-function "onAnimationEnd" and the CSS classes can now be overwritten in the constructor.

    Cheers

    Philipp

    /*

    * Accordion Class – requires Prototype 1.6+ and Script.aculo.us Effects 1.8+

    */

    var Accordion = Class.create({

    initialize: function(options) {

    this.accordion = $(options.id);

    if (!this.accordion) {

    throw ("Attempted to initalize accordion with undefined element: " + id);

    }

    this.toggleClass = options.toggleClass || "accordion-toggle";

    this.activeClass = options.activeClass || "active";

    this.contentClass = options.contentClass || "accordion-content";

    this.onAnimationEnd = options.onAnimationEnd || function() {};

    this.type = options.type || ‘vertical’;

    this.contents = this.accordion.select(‘.’ + this.contentClass);

    this.current = this.accordion.select(‘.’ + this.activeClass)[1];

    this.isAnimating = false;

    this.toExpand = null;

    if(this.type == ‘vertical’) {

    this.maxHeight = this.contents.max(function(e) {

    return e.getHeight();

    });

    }

    this.initialHide();

    if(this.current) {

    this.current.previous(‘.’ + this.toggleClass).addClassName(this.activeClass);

    if (this.type == ‘vertical’ && this.current.getHeight() != this.maxHeight) {

    this.current.setStyle({height: this.maxHeight + "px"});

    }

    }

    options.event = options.event || ‘click’;

    this.accordion.select("." + this.toggleClass).invoke("observe", options.event, this.toggle.bindAsEventListener(this));

    },

    toggle: function(e) {

    var el = e.element();

    if (!this.isAnimating) {

    this.toExpand = el.next(‘.’ + this.contentClass);

    this.animate();

    e.stop();

    return false;

    }

    },

    initialHide: function() {

    this.contents.each(function(e) {

    e.setStyle({‘display’: ‘block’});

    if (e == this.current) {

    return;

    }

    if(this.type == ‘horizontal’) {

    e.setStyle({width: 0});

    }

    else {

    e.setStyle({height: 0});

    }

    }.bind(this));

    },

    animate: function() {

    var effects = [];

    var on_options = {

    sync: true,

    scaleFrom: 0,

    scaleContent: false,

    scaleMode: ‘contents’,

    scaleX: false,

    scaleY: true

    };

    var off_options = {

    sync: true,

    scaleContent: false,

    scaleX: false,

    scaleY: true

    };

    if(this.type == ‘vertical-multiple’) {

    if(this.toExpand.previous(‘.’ + this.toggleClass).hasClassName(this.activeClass)) {

    effects.push(new Effect.Scale(this.toExpand, 0, off_options));

    this.toExpand.previous(‘.’ + this.toggleClass).removeClassName(this.activeClass);

    this.toExpand.removeClassName(this.activeClass);

    }

    else {

    effects.push(new Effect.Scale(this.toExpand, 100, on_options));

    this.toExpand.previous(‘.’ + this.toggleClass).addClassName(this.activeClass);

    this.toExpand.addClassName(this.activeClass);

    }

    }

    else {

    if (this.toExpand == this.current) {

    return;

    }

    if(this.type == ‘horizontal’) {

    on_options.scaleX = off_options.scaleX = true;

    on_options.scaleY = off_options.scaleY = false;

    }

    else {

    on_options.scaleMode = {originalHeight: this.maxHeight, originalWidth: this.accordion.getWidth()};

    }

    effects.push(new Effect.Scale(this.toExpand, 100, on_options));

    effects.push(new Effect.Scale(this.current, 0, off_options));

    this.current.previous(‘.’ + this.toggleClass).removeClassName(this.activeClass);

    this.current.removeClassName(this.activeClass);

    this.toExpand.previous(‘.’ + this.toggleClass).addClassName(this.activeClass);

    this.toExpand.addClassName(this.activeClass);

    }

    new Effect.Parallel(effects, {

    duration: 0.75,

    fps: 25,

    queue: {position: ‘end’, scope: this.accordion.id + "Animation"},

    beforeStart: function() {

    this.isAnimating = true;

    }.bind(this),

    afterFinish: function() {

    this.current = this.toExpand;

    this.isAnimating = false;

    this.onAnimationEnd();

    }.bind(this)

    });

    }

    });

  4. This is simply brilliant. I have had similar cases as well, that I just developed my own code based on others already available that were either not perfect or too much.

    However, I have a question: would it be able to put a vertical accordion inside a horizontal one?

  5. I’m noticing the width of the accordian shrinking after the first annimation, but only on the horizontal example. This is occuring in firefox 3.6 as well as IE8. Everything works just fine in chrome. Anyone else seeing this? Any suggestions on fixing?

  6. This is great but I don’t want a fixed height vertical accordion; I need it be as flexible as the original stickmanlabs version (which no longer works with the current prototype and script.aculo.us effects). How could I modify this to not use maxHeight?

  7. Great stuff. But how do I choose which accrodion slide / option to open by default when it loads. It is always the second slide…

  8. Been digging for something like this for days. I’m by no means a js wiz but tried and failed to nest accordions properly.

    Well nesting works but untill you toggle the nested accordion. That results in the vertical one collapsing…

    I’m looking to do a "3×3" accordion where the center piece is a slider. Would love some feedback on this.

  9. This script was missing a bit of error checking if you hadn’t assigned the ‘active’ class to both a toggle and matching content element.

    You’d see an error in firebug saying:

    ‘uncaught exception: [object Object]‘

    I’ve not looked to see what the best way of fixing it really is, but so far this works.

    At around line 112 add an ‘if’ wrapper as follows:

    if(this.current) {

    effects.push(new Effect.Scale(this.current, 0, off_options));

    this.current.previous(‘.’ + this.toggleClass).removeClassName(this.activeClass);

    this.current.removeClassName(this.activeClass);

    }

    Hope this helps someone :)

  10. Great work!

    I needed a feature to be able to "reset" the accordion to the original value. Although I’m sure there are other ways to do this, but I added this function to accordian.js:

    toggleReset: function(el) {

    if (!this.isAnimating) {

    this.toExpand = el.next(‘.’ + this.contentClass);

    this.animate();

    return false;

    }

    },

    Then in my UI I have a reset button that calls another method, which does this:

    resaccordion.toggleReset($(‘results-accordion-searchtab’));

    where my accordion has this defined for the default tab with an id:

    Search Results

  11. Him, I’m stuck. I inserted the JS into my html head.

    And also:

    document.observe("dom:loaded", function() {

    accordion = new Accordion({id: "test-accordion", type: ‘vertical’});

    });

    Problem is, on pages of my site that don’t contain the accordian, I am getting JS error.

    Line 8. ID not defined.

  12. I really like the JavaScript Accordion that you have created. It works really well and smooth in multiple browsers.

    I have a site designed with tables (Booo). I would like to place your script within a table. Works great in IE8. A soon as I turn on compatibility mode to simulate older versions I get a lot of "jumping" on hover and on expanding. It seems to have something to do with a boarder as when I do the compatibility mode and hover I can see the space between the slider increase. I have played around with the CSS in an attempt to isolate this, but I am starting to pull my hair out. HELP!

  13. Mike,

    Thanks, glad this is useful. I have noticed in some browsers, especially IE6, that there is jumpiness when the accordion contains tables. It can be minimized, but not completely resolved, by adjusting the margin and padding of the table. If this fails, try wrapping the content with a div.

    In the end, it’s difficult balancing the effects performed by Prototype and rendering of the elements styles by the browser. As such, I live with the minimal jumpiness when my content contains tables. But I may revisit this in time.

  14. When I put "active" on any other it doesn’t work, only works on the second one.

    I have reduced it to this bare minimum:

    Accordion

    #acc1 {position: relative;}

    .accordion-toggle {padding:0;margin:0;cursor:pointer;}

    .accordion-content {overflow: hidden;}

    document.observe("dom:loaded", function(){

    accordion = new Accordion({id:"acc1",type:’vertical-multiple’});

    });

    One

    Lorem ipsum dolor sit

    Two

    Lorem ipsum dolor sit

    Three

    Lorem ipsum dolor sit

    Four

    Lorem ipsum dolor sit

  15. I’ve download your source code and attempt to insert your test accordion into my application. in hole it works nice but I’ve got a little trouble – hide and show effects occurs twice: previous content hides, current open, then current hides and open another time. Have you any ideas what i’am doing wrong?

  16. Great accordion, thank you for sharing it with us. Just one question: Is there a way to implement an "auto-slide" feature, so it "tabs" through the divs automatically every X seconds?

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>