Advertisement

Create a Simple, Intelligent Accordion Effect Using Prototype and Scriptaculous

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

We've all seen the "accordion" type effect used on many Web 2.0 sites; however, many accordion scripts are heavy, make poor use of the libraries they are based on, and don't handle things like ensuring that the accordion maintains a consistent height. In this tutorial, we will use the Prototype and Scriptaculous libraries to create a lightweight, intelligent accordion.


Demo and Source Code



Step 1 - The Goal

Our objective is to create a lightweight accordion script based on the Prototype and Scriptaculous javascript libraries.
The accordion should:

  • Allow an unlimited number of accordion panes
  • Be completely styled by CSS
  • Be Unobtrusive—users without javascript turned on should see all of your accordion content
  • Be Light Weight—With relatively few lines of code; use event delegation to limit memory consumption.
  • Support any kind of content within the accordion
  • Ensure that when the content of each accordion pane changes, the height of the accordion remains constant to avoid
    the annoying "page bouncing" effect

This is a relatively advanced tutorial that assumes the reader has a reasonable knowledge of Javascript, CSS, HTML, Object-Oriented
programming, and a basic understanding of the Prototype and Scriptaculous libraries. However, complete source code is
available for you to study and the code is very simple to read and learn from if you are not familiar with the specific
libraries used.

Before we begin, you can see a working demonstration of the accordion in action.


Step 2 - Begin with Basic Markup

To begin, we will create some simple HTML markup for our accordion:

<div id="test-accordion" class="accordion">    
     <div class="accordion-toggle">Toggle 1</div>    
     <div class="accordion-content">Content 1</div>    
     <div class="accordion-toggle">Toggle 2</div>    
     <div class="accordion-content">Content 2</div>    
     <div class="accordion-toggle">Toggle 3</div>    
     <div class="accordion-content">Content 3</div>    
     <div class="accordion-toggle">Toggle 4</div>    
     <div class="accordion-content">Content 4</div>
</div>

Step 3 - Add Some Style

Next, we need to add some style around our accordion to make it look like an accordion. To begin with, we'll do a first pass of basic styling and then add more when it's all working. There are also some additional
styles that need to be included to ensure that the accordion will display correctly as it is animating.

div#test-accordion{
    margin: 10px;
    border: 1px solid #aaa;}

div.accordion {
    position: relative; /* required for bounding--works around a "peculiarity" in Prototype */
}

div.accordion-toggle{
    position: relative; /* required for effect */
    z-index: 10;		/* required for effect */
    background: #eee;   /* required for effect--can be anything except "transparent" */
    cursor: pointer;
}

div.accordion-toggle-active{
    background: #fff;
}

div.accordion-content{
    overflow: hidden;	/* required for effect */
    background: #aaa;
}

See the basic accordion with a simple stylesheet.

Step 4 - Create the Javascript Accordion Class

Prototype provides a wonderful framework for building classes in Javascript and we'll use that functionality to build
our accordion class. This class will contain all the properties and methods of an accordion: the currently displayed
pane, the contents of the accordion, methods to expand and contract panes, and event handler methods to define what happens
when users take an action such as clicking. For now, we'll set up the basic structure of the class as well as all the
properties and methods we'll need:

var Accordion = Class.create({
    initialize: function(){
        this.accordion = null;           /* Stores a pointer to the the accordion element */
        this.contents = null;            /* Array of pointers to the headings and content panes of the accordion */
        this.options = null;             /* Allows user to define the names of the css classes */
        this.maxHeight = 0;              /* Stores the height of the tallest content pane */
        this.current = null;             /* Stores a pointer to the currently expanded content pane */
        this.toExpand = null;            /* Stores a pointer to the content pane to expand when a user clicks */
        this.isAnimating = false;        /* Keeps track of whether or not animation is currently running */

    },

    checkMaxHeight: function(){},         /* Determines the height of the tallest content pane */
    initialHide: function(){},            /* Hides the panes which are not displayed by default */
    attachInitialMaxHeight: function(){}, /* Ensures that the height of the first content pane matches the tallest */
    expand: function(el){},               /* Tells the animation function which elements to animate */
    animate: function(){},                /* Performs the actual animation of the accordion effect */
    handleClick: function(e){}            /* Determine where a user has clicked and act based on that click */

});

These are the basic methods and properties that we will need when building our accordion. Each of the next steps will
take you through building each method until we have a working accordion. If at any point during the tutorial you need
a quick refresher on what each method or property is for, you may use this heavily commented code as a reference.


Step 5 - Initialize: Get Things Started

Prototype classes have a special method called initalize() which is a constructor; this means it acts when the user
creates a new instance object of that class. For any accordion, we need to know 2 things before we begin:

  1. The id of the accordion element.
  2. The default starting position of the accordion (if anything other than the first position)

So, we will need to allow our constructor to accept those two parameters. Additionally, our constructor must:

  1. Retrieve and store the accordion and its contents as pointers to those elements
  2. Set the user defined options
  3. Set the current expanded element
  4. Determine the maximum height we will use as the height for all of our content panes and apply it
  5. Hide the content panes which are not shown by default
  6. Add an event listener to the accordion to watch user clicks.

Here is the code for our initialize() method:

initialize: function(id, defaultExpandedCount) {
    if(!$(id)) throw("Attempted to initalize accordion with id: "+ id + " which was not found.");
    this.accordion = $(id);
    this.options = {
        toggleClass: "accordion-toggle",
        toggleActive: "accordion-toggle-active",
        contentClass: "accordion-content"
    }
    this.contents = this.accordion.select('div.'+this.options.contentClass);
    this.isAnimating = false;
    this.maxHeight = 0;
    this.current = defaultExpandedCount ? this.contents[defaultExpandedCount-1] : this.contents[0];
    this.toExpand = null;

    this.checkMaxHeight();
    this.initialHide();
    this.attachInitialMaxHeight();

    var clickHandler =  this.clickHandler.bindAsEventListener(this);
    this.accordion.observe('click', clickHandler);
}

As you can see, we've set all of our properties to reasonable default values and called 3 methods to help get things set
up. Finally, we've attached the event handler to the accordion. Let's create those three methods and the event handler.


Step 6 - Checking the Tallest Element

One of the requirements for our accordion is that it must scale so that even when the tallest content pane is expanded,
the overall accordion height will remain constant. To accomplish this goal, we will iterate through the content panes
determining which one is the tallest and set the maxHeight property accordingly:

checkMaxHeight: function() {
    for(var i=0; i<this.contents.length; i++) {
        if(this.contents[i].getHeight() > this.maxHeight) {
            this.maxHeight = this.contents[i].getHeight();
        }
    }
}

Step 7 - Hiding the Rest

Our accordion should only display the content pane specified as the current pane; all others should be hidden
by default. Additionally, we need to set these content pane's height attribute to 0; this prevents the content pane from
briefly appearing fully expanded before properly animating.

initialHide: function(){
    for(var i=0; i<this.contents.length; i++){
        if(this.contents[i] != this.current) {
            this.contents[i].hide();
            this.contents[i].setStyle({height: 0});
        }
    }
}

Step 8 - Show the Default Content Pane

Now that we've hidden all but the default content pane, we need to make sure the default content pane displays correctly;
it's heading should have the "active" style applied to it and it's height should match the maxHeight property:

attachInitialMaxHeight: function() {
    this.current.previous('div.'+this.options.toggleClass).addClassName(this.options.toggleActive);
    if(this.current.getHeight() != this.maxHeight) this.current.setStyle({height: this.maxHeight+"px"});
}

Step 9 - Create the Event Handler

If you come from a traditional event handling background where we attach the event handler to each area we want clickable,
it may seem confusing that we are only attaching the handler to one element. We are using event
delegation
. For those of you unfamiliar with the subject, I have written a brief
overview of event delegation
which
will introduce you to the concept and why it is so important. That said, we need an intelligent event handler:

clickHandler: function(e) {
    var el = e.element();
    if(el.hasClassName(this.options.toggleClass) && !this.isAnimating) {
        this.expand(el);
    }
}

There are two parts to this function. First, we determine what was clicked. Then, we check to make sure that it was a
heading that was clicked and that no animation is currently running. If this is the case, we call the expand() method
to start the process of the accordion. The variable we pass to the expand() method is the heading on which the user clicked.


Step 10 - Start the Process

Now we can start the process of doing the accordion effect. We know the expand() method must take a parameter for the
element which was clicked. Using that parameter, the expand method determines which content pane to expand, and if it
is not already expanded, calls the animate() method to "do its magic!"

expand: function(el) {
    this.toExpand = el.next('div.'+this.options.contentClass);
    if(this.current != this.toExpand){
	    this.toExpand.show();
        this.animate();
    }
},

Step 11 - Doing the "Dirty Work"

At this point, all of the pieces are in place; we know which content pane is currently displayed, we know which heading
the user has clicked, and we know which content pane the user has requested to be shown. Now, we must create the accordion
animation. For this, we will create an animate() method that will use the Scriptaculous Effect.Parallel class to render
the two animations together; and the Effect.Scale class to change the size of each content pane. The animate method will
perform these steps:

  1. Create an array that will be used to store our Effect.Scale objects
  2. Collect the parameters to pass to the Effect.Scale constructor for the content pane that will be shown and create
    the object
  3. Add that object to our array
  4. Collect the parameters to pass to the Effect.Scale constructor for the content pane that will be hidden and create
    the object
  5. Add that object to our array
  6. Create the Effect.Parallel object that will run our Effect.Scale objects is sync.
  7. Tell our Accordion object that we are animating
  8. Run the animations
  9. Clean up any styles left behind
  10. Tell our Accordion object that we are finished animating
animate: function() {
    var effects = new Array();
    var options = {
        sync: true,
        scaleFrom: 0,
        scaleContent: false,
        transition: Effect.Transitions.sinoidal,
        scaleMode: {
            originalHeight: this.maxHeight,
            originalWidth: this.accordion.getWidth()
        },
        scaleX: false,
        scaleY: true
    };

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

    options = {
        sync: true,
        scaleContent: false,
        transition: Effect.Transitions.sinoidal,
        scaleX: false,
        scaleY: true
    };

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

    new Effect.Parallel(effects, {
        duration: 0.5,
        fps: 35,
        queue: {
            position: 'end',
            scope: 'accordion'
        },
        beforeStart: function() {
            this.isAnimating = true;
            this.current.previous('div.'+this.options.toggleClass).removeClassName(this.options.toggleActive);
            this.toExpand.previous('div.'+this.options.toggleClass).addClassName(this.options.toggleActive);
        }.bind(this),
        afterFinish: function() {
            this.current.hide();
            this.toExpand.setStyle({ height: this.maxHeight+"px" });
            this.current = this.toExpand;
            this.isAnimating = false;
        }.bind(this)
    });
}

For a complete explanation of the option parameters we are passing to both the Effect.Scale and Effect.Parallel objects,
please see the Scriptaculous documentation.
The important aspects of the method are the beforeStart and afterFinish methods on our Effect.Parallel. The beforeStart
method tells the accordion that it is currently animating. This will prevent the event handler from attempting to start
any further changes so long as the animation is in progress. It also makes sure that the heading which was clicked is
given the "active" class name. The afterFinish method completely hides the content pane which had been previously displayed
(after it has disappeared due to the animation). It also ensures that the final height of the newly displayed content
pane is correct. Now that the swap is complete, it tells our accordion that the currently expanded content pane is the
one we have newly expanded and that the animation is complete.


Step 12 - Adding Some More Style

At this point we have a decent looking accordion, which you can see in action here. But with a little CSS we can make it all look much more spectactular. So first we create a quick Photoshop mockup so we have a rough idea of how it should all look. With that in mind, we're going to need three images:

  1. A 'logo' image -
  2. A couple of nice background images - and

And here's the revised CSS code:

body {
	padding: 130px 50px 50px 50px;
	background: #252422 url(../img/logo.gif) no-repeat;
	background-position: 60px 40px;
	font-family: "Lucida Grande", "Lucida Sans Unicode", Arial, Sans-serif;
	font-size: 11px;
	line-height: 18px;
}

div#test-accordion{
	border: 1px solid #343230;
	background-color: #21201f;
	padding: 10px;
}

div.accordion {
	position: relative; /* required for bounding */http://net.tutsplus.com/wp-admin/users.php
	width: 800px;
}

div.accordion-toggle{
	position: relative; /* required for effect */
	z-index: 10;		/* required for effect */
	background: #3f3c38 url(../img/off.jpg) repeat-x;
	background-position: bottom;
	color: #fff;
	cursor: pointer;
	margin-bottom: 1px;
	padding: 9px 14px 6px 14px;
	border-top: 1px solid #5d5852;
}

div.accordion-toggle:hover, div.accordion-toggle-active{
	background-image: url(../img/on.jpg);
	background-color: #6d493a;
	border-top: 1px solid #a06b55;
}

div.accordion-content{
	overflow: hidden;	/* required for effect */
	background: #302e2c;
	color: #c4bab1;
	border-bottom: 1px solid #000;
}

div.accordion-content p{
margin: 9px 24px 6px 24px;
}

As you can see here we've:

  1. Added some background styles around the page and the accordion class
  2. Given the accordion-toggle div a regular background color
  3. Set the accordion-toggle:hover and the active states to use the same reddish background

Step 13 - See It in Action

You can see the working demonstration here. You can also add your own CSS and images
to tailor the look to your site.

Download: accordion.js & accordion.css

Advertisement