Creating expandable & collapsible panels in jQuery, CSS & HTML

18 July, 2013 by Tom Elliott

The use of expandable and collapsible panels is widespread on the Internet. There are many plugins and code available that allow you to implement various types of expandable content or ‘accordion’ effects which essentially involve the user clicking on a panel heading to reveal the content underneath. Expandable panels are often used as a way to break up content rich pages into more visually appealing sections, where visitors can choose to read more about a particular section if they wish.

This tutorial walks through the processes and code required to create your own version of expandable panels using a mix of HTML, CSS and jQuery. Modifying and styling the panels is easy and the code has been tested on multiple browsers and devices including Internet Explorer 8, 9 and 10 as well as Chrome, FireFox and Safari. The expandable panels are also responsive and fit 100% of the parent container.

Take a look at the expandable panel demo to see what we will be replicating. There are 3 content panels which can be expanded and collapsed and an open/close icon to indicate the selected panel open/close state.

This code also has some optional parameters that can allow the following to be configured:

  1. Accordion effect – allows the panels to be turned into a typical Accordion whereby selecting a closed panel will collapse any open panels.
  2. Default open panel – selects which panel (if any) to expand as soon as the page is loaded
  3. Panel speed – selects how fast to expand or collapse the panel content

The HTML

The HTML for the expandable panels is fairly straightforward. Each of the 3 panels has a container div with the class expandable-panel and unique ID specified by “cp-1”, “cp-2” etc, which the jQuery references. Within each of these panels is a heading div that contains the open/close icon and a content div which contains regular HTML.

<div id="container">
 	<div class="expandable-panel" id="cp-1">
    	<div class="expandable-panel-heading">
        	<h2>Content heading 1<span class="icon-close-open"></span></h2>
         </div>
    	<div class="expandable-panel-content">
        	<p>Panel HTML...</p>
        </div>
    </div>

    <div class="expandable-panel" id="cp-2">
    	<div class="expandable-panel-heading">
        	<h2>Content heading 2<span class="icon-close-open"></span></h2>
         </div>
    	<div class="expandable-panel-content">
        	<p>Panel HTML...</p>

        </div>
  </div>

  <div class="expandable-panel" id="cp-3">
     <div class="expandable-panel-heading">
         <h2>Content heading 3<span class="icon-close-open"></span></h2>
     </div>
     <div class="expandable-panel-content">
         <p>Panel HTML...</p>
     </div>
  </div> 

</div>

The CSS

The class for the main content expandable-panel-content has the margin-top initially set to -999 pixels. This is to avoid the content from being briefly shown before the page has loaded and the JavaScript has determines the height of the content panel and its position relative to its container.

The main expandable-panel class is given the overflow:auto; property which allows the panel to expand as the content div is revealed and will keep the content hidden when it is outside the div boundaries. This container also needs a minimum height setting so that its height isn’t determined by the negative value of the content panel.

The only image used in the CSS is the icon for the open/close status which you can grab here or recreate.


h2, p, ol, ul, li {
	margin:0px;
	padding:0px;
	font-size:13px;
	font-family:Arial, Helvetica, sans-serif;
}

#container {
	width:300px;
	margin:auto;
	margin-top:100px;
}

/* --------- COLLAPSIBLE PANELS ----------*/

.expandable-panel {
    width:100%;
    position:relative;
    min-height:50px;
    overflow:auto;
    margin-bottom: 20px;
	border:1px solid #999;
}
.expandable-panel-heading {
    width:100%;
    cursor:pointer;
    min-height:50px;
    clear:both;
    background-color:#E5E5E5;
    position:relative;
}
.expandable-panel-heading:hover {
    color:#666;
}
.expandable-panel-heading h2 {
    padding:14px 10px 9px 15px;
    font-size:18px;
    line-height:20px;
}
.expandable-panel-content {
    padding:0 15px 0 15px;
    margin-top:-999px;
}
.expandable-panel-content p {
    padding:4px 0 6px 0;
}
.expandable-panel-content p:first-child  {
	padding-top:10px;
}
.expandable-panel-content p:last-child {
    padding-bottom:15px;
}
.icon-close-open {
    width:20px;
    height:20px;
    position:absolute;
    background-image:url(icon-close-open.png);
    right:15px;
}
.expandable-panel-content img {
	float:right;
	padding-left:12px;
}
.header-active {
    background-color:#D0D7F3;
}

The jQuery

This is where the fun begins! There are 3 functions here. panelinit() initialises the expandable panels and sets the height and position for each panel content. We execute the initialisation function after the page has loaded, to give a chance for any images used within the panels to be loaded so the heights can be better determined.

The main expanding/collapsing function is called when a panel heading is clicked, and animates the margin-top style of the associated content panel to either zero or the starting position depending on whether the panel is open or closed.

The last function resetpanels() is called only if the accordion variable is set to true, which will collapse any existing open panels so only one panel can ever be fully expanded at once.

To change the animation speed, adjust the panelspeed variable (currently set to half a second). If you want a panel to be initially expanded when the page loads, specify the panel number with the defaultopenpanel variable. You will also need to specify the total panels being used using totalpanels.

(function($) {
    $(document).ready(function () {
        /*-------------------- EXPANDABLE PANELS ----------------------*/
        var panelspeed = 500; //panel animate speed in milliseconds
        var totalpanels = 3; //total number of collapsible panels
        var defaultopenpanel = 0; //leave 0 for no panel open
        var accordian = false; //set panels to behave like an accordian, with one panel only ever open at once      

        var panelheight = new Array();
        var currentpanel = defaultopenpanel;
        var iconheight = parseInt($('.icon-close-open').css('height'));
        var highlightopen = true;

        //Initialise collapsible panels
        function panelinit() {
                for (var i=1; i<=totalpanels; i++) {
                    panelheight[i] = parseInt($('#cp-'+i).find('.expandable-panel-content').css('height'));
                    $('#cp-'+i).find('.expandable-panel-content').css('margin-top', -panelheight[i]);
                    if (defaultopenpanel == i) {
                        $('#cp-'+i).find('.icon-close-open').css('background-position', '0px -'+iconheight+'px');
                        $('#cp-'+i).find('.expandable-panel-content').css('margin-top', 0);
                    }
                }
        }

        $('.expandable-panel-heading').click(function() {
            var obj = $(this).next();
            var objid = parseInt($(this).parent().attr('ID').substr(3,2));
            currentpanel = objid;
            if (accordian == true) {
                resetpanels();
            }

            if (parseInt(obj.css('margin-top')) <= (panelheight[objid]*-1)) {
                obj.clearQueue();
                obj.stop();
                obj.prev().find('.icon-close-open').css('background-position', '0px -'+iconheight+'px');
                obj.animate({'margin-top':0}, panelspeed);
                if (highlightopen == true) {
                    $('#cp-'+currentpanel + ' .expandable-panel-heading').addClass('header-active');
                }
            } else {
                obj.clearQueue();
                obj.stop();
                obj.prev().find('.icon-close-open').css('background-position', '0px 0px');
                obj.animate({'margin-top':(panelheight[objid]*-1)}, panelspeed);
                if (highlightopen == true) {
                    $('#cp-'+currentpanel + ' .expandable-panel-heading').removeClass('header-active');
                }
            }
        });

        function resetpanels() {
            for (var i=1; i<=totalpanels; i++) {
                if (currentpanel != i) {
                    $('#cp-'+i).find('.icon-close-open').css('background-position', '0px 0px');
                    $('#cp-'+i).find('.expandable-panel-content').animate({'margin-top':-panelheight[i]}, panelspeed);
                    if (highlightopen == true) {
                        $('#cp-'+i + ' .expandable-panel-heading').removeClass('header-active');
                    }
                }
            }
        }
       
       //Uncomment these lines if the expandable panels are not a fixed width and need to resize
       /* $( window ).resize(function() {
          panelinit();
        });*/

        $(window).load(function() {
 			panelinit();
        }); //END LOAD
    }); //END READY
})(jQuery);

Update 01/10/2013: Added ‘header-active’ class to highlight the heading of any active and open panels. This can be turned off by setting highlightopen to false

Update: 07/08/2014. Added resize function for responsive expandable panels that don’t have a fixed width. Just uncomment the $(window).resize function in the jQuery above

 



110 Comments

  • JC says:

    Will these work with a fusion chart inside? Charts need a starting height for their parent container when they first render. Will these render a chart in their collapsed state?

    • Tom Elliott says:

      Hey JC, if the Charts need the height to be defined in the CSS, rather than allowing the panels to automatically expand then they probably won’t work. I haven’t used fusion charts though so can’t say for sure.

  • Ian says:

    totally a newb question i know, but what do i need to change so that I can put the jquery code in a separate file?

    Thanks

    • George says:

      You open a text document and enter the text.
      Then you go save as and make sure the extension is file.js then hit save that should work!

      Note: when you want to make changes you have to go file open with notepad, then save as again, change from text file to all files and find the new file.js file and save over the top of it.

      That’s it.

      • Tony Pratt says:

        For some reason the script works when I place it in the page head but not when I save to a separate file and link to the js file. The CSS file links fine but not the js. Any ideas?

        • Tom Elliott says:

          Hi Tony, you should be fine to put the panel JS in a separate file… does the JS file come after the jQuery link? If you’re using WordPress, sometimes the order of the files can be changed

  • Josh says:

    I tried implementing this and it didnt work like it should, the panels just appeared with scroll wheels on their ends and clicking does nothing as if the javascript was broken.

    Any help?

  • Josh says:

    Sorry I don’t have online as of yet, but i was wondering if it might have something to do with my javascript or html heads since im using an old template to make my site or im not linking correctly.

    Thanks for the speedy reply by the way

    • Tom Elliott says:

      Hi Josh, ah yes – your old template might be causing a jQuery conflict or could be an other issue… maybe even if your old CSS shares the same classes as the panels. If your JS console isn’t giving clues, I would be tempted to get the panels working independently first, then copy the HTML into the panels and then integrate the panels within the template. 🙂

  • Robert Oberg says:

    Is there a simple way that I can change the color of the active header in this accordion? I have spent so much time trying to figure it out. Thank you so much for taking the time to help out in this.

    • Tom Elliott says:

      Hi Robert, that’s a good feature suggestion. I’ve updated the expandable panel demo and code so that it adds a ‘header-active’ class which sets the background colour of the header for the active panel(s).

  • Marlskie says:

    Thanks tom great share!!!!!

  • Maurice says:

    Hi.. is there a simple way to have all panels already opened when the page loads instead of just the default panel?

    • Maurice says:

      Nevermind I figured it out.. removed if (defaultopenpanel == i) { … }
      and left the two lines.

      $(‘#cp-‘+i).find(‘.icon-close-open’).css(‘background-position’, ‘0px -‘+iconheight+’px’);
      $(‘#cp-‘+i).find(‘.expandable-panel-content’).css(‘margin-top’, 0);

  • Brad says:

    Is it possible to have 2 windows open by default instead of just 1?

    I have 4 panels and if possible I would like to have 2 of 4 default to open on a page. I’m fairly new to Jquery and I’ve been poking around to no avail.

    • Tom Elliott says:

      Hi Brad, although not tested, in the panelinit function you could try changing the line to:
      if ((defaultopenpanel == 1) || (defaultopenpanel == 2)) {
      Where 1 and 2 are the panels you want expanded by default.

      • Brad says:

        Thank you for taking the time to respond to my question. Unfortunately that seems to force all panels open for some reason. I will keep playing with it though.

  • Suzanne says:

    Hi- I have an older version of ie and only the last panel clicks and animates…none of the other panels work, but on newer ie, chrome and firefox, they all work great. I have searched and searched for a solution to this but can’t find any…help!

    • Tom Elliott says:

      Hi Suzanne, what version of Internet Explorer are you using? I haven’t tested the expandable panels on anything below IE 8 but I’ll try and take a look when I get the chance

  • Kathy says:

    Thanks for this, it’s what I’ve been looking for.

    I would like to use a different icon for open and close and also show hover states for open and close. I currently have a sprite for this, with icons 25px apart (vertically). How would I achieve this please?

    • Kathy says:

      I should probably also include the sprite positions:
      open: 0, 0
      open hover: 0, 25
      close: 0, 50
      close hover: 0, 75

    • Tom Elliott says:

      Hi Kathy,

      Ah OK, yeah this should be possible. With this current demo, the icon just has an open and close state but you should be able to add the below CSS to target the hover states for both open and closed panels. You probably also need an !important in there to overwrite any inline CSS the jQuery uses. (I haven’t tested this BTW so hope it works!) 😉


      .expandable-panel-heading:hover .icon-close-open {
      background-position:0px -75px !important;
      }

      .header-active:hover .icon-close-open {
      background-positition:0px -25px !important;
      }

  • Carl says:

    This is great!
    I’m just having a little problem when using responsive content. For instance when flipping a handheld from vertical to horizontal, some of the contents at the bottom gets showned.. It ofcourse works after page refresh.

    I got around it by setting a fixed height to my content, but I wonder if there is a better solution or something I’m missing. (Wolud like to keep it as repsonsive as possible).

    Help needed. Thank you!

    • Tom Elliott says:

      Hi Carl, yeah I think I know what you mean but if you have a link I can take a look :). You could try setting overflow:auto; to the CSS of the content containing the panels which might help.

      • Trent says:

        I have experienced some of the same problems that Carl did above. Sometimes the expanded content shows between 1/6th of a line and up to 3 lines and it doesn’t seem to be related to the size of the content. It currently only seems to show up on mobile devices, but inconsistently. Tried on iPhone 4 &5 and Samsung Tab. Sometimes the content doesn’t show at all until opened once, and then it only partially closes. It seems to work better with overflow set to hidden rather than auto. Ideas?

  • Christo says:

    HI Tom. Thanks for the great post. I have a question though: Is there anyway to close a panel manually. My page startup correctly with two divs collapsed. A table gets build in the first panel when dong a action on the second panel. The first panel opens up by itself. This all happens with javascript.

    • Tom Elliott says:

      Hi Christo, thanks – it sounds like the first panel will appear partially open after the table gets added because the height is now different (and heights of panels are calculated on page load). A good starting point might be to try and call panelinit(); function again when the action on the second panel is triggered..

  • Mike says:

    Hi Tom.
    For some reason, my panels don’t open. The + and the X are no where to be found either. Any ideas?

  • John says:

    This works fine on desktop but not working on mobile. I am using exactly the same JS, css and html on our mobile site and it shows up and looks great. Clicking the plus image opens the panels but cannot close them. Once they are opened they cannot be closed although working perfectly on desktop.

    I don’t know if it is related but also setting var accordian = true; doesn’t do anything. it still allows all of them to be opened and not just one at a time.

    Any suggestions?

    Awesome post by the way, exactly what I was looking for 🙂

  • john says:

    Hi Tom, I am testing using browserstack so was using ios buy looking on my galaxy note 2, same issue.

    Here is the link [retracted]

    Coumd it be a jquery conflict?

  • john says:

    By the way, your demo works fine so its just mine that has the issue.

  • Chad says:

    Hi Tom. Thank you so much for this! It was super helpful! I am having two slight difficulties with this (I’m not the best with all this stuff).
    Problem 1: I set it to automatically load with the first panel open, however when the page loads the open panel doesn’t display the active color (which is blue). Instead, it loads with a black background (which is the color of the inactive/closed panels). You have to close it and reopen it again for it to show the active blue color.

    Problem 2: when you close the panel the inside text can still be seen slightly above the heading section. 🙁

    Could you please a take a look at this? I’d appreciate it a ton!

  • Chad says:

    Hi Tom. Thank you so much for this! It’s been great. I have two problems I was hoping you could shed some light on?
    1: I set it so that one panel loads already open. However, when it loads it doesn’t display the active color (which I set to blue). You have to close the panel and re-open it again to show as blue. How can I fix this so that when it loads the open panel shows the active header color?

    2: When I close the panels a bit of text can still be seen above the header. If it helps any, the outline in .expandable-panel doesn’t wrap around just the header––there’s extra room above and below. Not sure if that’s related but it seems like it is. If you could shed any light on these problems i would be so grateful. Thank you again for this! I’ve been looking all over for this!!

    Chad

  • tina says:

    Hi,

    Thanks for the code. I just have one problem. I have another two panels that want to put in one panel as sub-panels. Lets say I have panel 1 and inside that I want to have panel 2 and 3… When I close panel 1 it shows panel 3 on top of 2 and 2 on top of 1… What should I do to fix this?

  • Ugo says:

    i have applied this code of my html page and its not working…what could be the issue?

  • baluku james says:

    the jquery which i saved as a javascript file doesnt load

  • John says:

    In case anyone was interested, I sorted my problem out. My desktop and mobile code is all in the same page and the javascript detects the device then uses desktop.css or mobile.css.

    As it is on the same page, it didn’t cross my mind that I couldn’t have 2 panels with the same id’s. So as I have 7 panels in total (4 on desktop and 3 on mobile), I changed the javascript to:

    var totalpanels = 7;

    and gave each panel a unique id. Problem solved.

  • Alan says:

    Why the ‘h2’ and ‘p’ classes in your CSS on line 1? I noticed that this conflicts with the existing styles of both h2 and p on my current site. Is there an alternative???

    Thank you.

    • Alan says:

      I solved the issue.

      Remove:

      h2, p, ol, ul, li {
      margin:0px;
      padding:0px;
      font-size:13px;
      font-family:Arial, Helvetica, sans-serif;
      }

      Add “margin: 0px;” to lines 35,44,47 and 50. This compliments better with the CSS on my site. Thank you. Great feature!

  • Justin says:

    Hi Tom,

    this is very useful, thank you. Unfortunately, there is a bug in Internet Explorer 11 with the 3rd panel in the example (with the floated image of penguins in it). The lower part of the image and the last line of text overhangs below the defined gray area of the box when collapsed. Funny that this doesn’t occur in IE8. Is there an easy fix?

    • Tom Elliott says:

      Hi Justin, thanks for letting me know about this – looks like the ‘clear:both’ style for .expandable-panel-content img was the culprit. Removing this seems to fix the issue. I’ve updated the post and code to reflect this. 🙂

  • serge says:

    demo works fine … panels look fine on my webpage … but they don’t open … any idea ?

  • Ashley Jaden says:

    This was EXACTLY what I was looking for, and worked like a charm! Thanks for structuring the code so neatly. T’was super simple to make style changes. You rock!!

  • bill bixby says:

    I found this very useful, thanks.

    I decided to tweak it a little so I could use it without having to specify how many panels there are. Set jQuery off counting the id attributes for cp-* then turned it into a generic script any page could use and include in the section.

    Next I used an array for specifying which panels I wanted open at the start.

    jquery.expandingpanels.js
    http://pastebin.com/5hhCTBJC

    Then I just call:

    $(document).ready(panelinit(new Array(‘panel1′,’panel2′,’etc’)));

  • Oli says:

    Hi Tom, thanks for this great resource, I have a quick question, and was wondering if you could advise?
    I have this setup to fit to the browser width, and an image occupying the top panel width:100%;.
    Because the panel height is set on page load, this means that when you resize the browser wider than it was when the page was loaded, the panel starts to protrude from the top. I’m not totally savvy with JS, so was wondering if you had any suggestions?

    here is a link btw, http://s435130782.websitehome.co.uk/op4/

    Thanks,

    • Oli says:

      Hey! A bit of google and a lot of staring at the .js file, I think I’ve fixed it, forgive me if its not correct, I haven’t the slightest clue what I’m doing with Javascript.

      For anyone having the same issue:
      Pop the following in under the ‘function resetpanels(){‘ bit

      function panelresize() {
      for (var i=1; i<=totalpanels; i++) {
      if (currentpanel == i) {
      panelheight[i] = parseInt($('#cp-'+i).find('.expandable-panel-content').css('height'));
      $('#cp-'+i).find('.expandable-panel-content').css('margin-top', +'0');
      } else {
      panelheight[i] = parseInt($('#cp-'+i).find('.expandable-panel-content').css('height'));
      $('#cp-'+i).find('.expandable-panel-content').css('margin-top', -panelheight[i]);

      }
      }
      }

      Then put the following in directly underneath where it says //END LOAD

      window.onresize=function(){
      panelresize();

      That seems to work for me. eg,
      http://s435130782.websitehome.co.uk/op5/

      Thanks again

      • Oli says:

        One small thing, now when I open a panel, then close it, then resize browser the last panel to be opened jumps open again. Oh well, sorry to plaster your page with messages.

  • Chris says:

    Hi Tom. This is a great tool – thanks for sharing it.

    One question regarding the sections. My script dynamically creates a table which I then insert into the expandable-panel-content section using document.getElemendById. However, when I retract the panel, it only retracts a small amount (the height of the original text), leaving most of the panel still visible. Is there a way to dynamically recalculate the amount the panel must contract?

    Thanks

    Chris

  • kd says:

    Is there any way we could save the closed and open state in cookie? That could be really useful. Thanks.

  • RyanG says:

    Great stuff!

    I have three separate pages within a website. Each page has a container div with a number of panels (the container on page 1 has 4 panels, the container on page 2 has 5 panels, and the container on page 3 has 3 panels). If I change the totalpanels value in the js file to 12 and then uniquely identify the panels with the cp-n value from 1-12, then the panels all expand and collapse as you would expect, independently. Each page has its own container div, but they are not uniquely named. Is this the best way to manage this or is there a better way? Thanks!

  • Craig says:

    Works great, thanks for sharing it, just one feature I’d want to add. How would you add buttons or links to “Expand All” and “Collapse All”?

  • Doug says:

    This is exactly the element I need. However, I am using an environment where I only have access to the of the page and cannot call external .js or .css files.

    Is this possible to execute this solely within the of the page? If so, would you be able to tell me how I should arrange all the code elements? Thanks!

    • Tom Elliott says:

      Hi Doug, unfortunately as this makes use of jQuery, it requires linking to an external jQuery file (either on-site or through Google CDN etc). Everything else should be embeddable in-line within the body tag… I suppose you could embed the entire jQuery file inline on the page but I would recommend against it if possible!

  • Doug says:

    Sorry, the word “body” is missing from “of the page” in a couple places. I had written it as an HTML tag and it was dropped.

  • Casey says:

    Would this work for nested panels? I have an application that works much like code collapsing that you see in code editors.

  • Vasyl says:

    A very neat script, thanks! But there is an issue in IE10 – it hides the scrollbars on page load. And when you click on any panel the windows jerks a bit. This issue is limited to IE only. Any suggestions will be appreciated. Thank you!

  • David says:

    Thanks for the script. Is it possible to have the content panel stay open when you move the cursor off of the heading panel and onto the content panel. Right now, once you move the cursor off the heading panel, the content panel retracts. Thanks.

  • Bella says:

    This works for me, but I have contents that are very long, I need a way to add a link at the bottom of the panel to close it.
    Any help is appreciated
    Thanks

  • muhammad ikram says:

    hi Tom . collapse is working good . i am using collapse . is there any way that it expend from bottom to top ?

  • Khalil says:

    Great Work. Can it work like the one discuss in this link: http://stackoverflow.com/questions/1861030/jquery-accordion-opening-a-box-based-on-href

    Basically i want to make it active through link. Is it possible here?

    • Tom Elliott says:

      Hi Khali, yes you should certainly be able to change the default panel that opens based on hash in the URL. Something like this could work.
      if (window.location.hash == "foo") { defaultopenpanel = 2; }