Tuesday, April 3, 2012

CSS3 Transition: Slideup Box (Take 2)

Demo and source code

This post is to propose a better solution for creating an expandable/slideup box. My previous implementation has a flaw -- the fixed "max-height" truncate part of the content when its height grows. This solution will resolve this issue.

First, we have the following markup.

<article>
  <h2>Click me to expand</h2>
  <div class="content_w">
    <div class="content">
      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do 
      eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 
      enim ad minim veniam, quis nostrud exercitation ullamco laboris 
      nisi ut aliquip ex ea commodo consequat...
    </div>
  </div>
</article>

When users click inside an <article>, the content area will slide down (expand) or slide up (collapse). The title (<h2>) will always be visible.

The idea is to wrap the content inside a wrapper (div.content_w). The wrapper will hide any overflown content. We will change the height of the wrapper to create a slide up/down effect.

article .content_w {
  overflow: hidden;
  height: 0;
}
article .content_w.transition {
  -webkit-transition: height 0.5s;
     -moz-transition: height 0.5s;
       -o-transition: height 0.5s;
          transition: height 0.5s;
}

The wrapper needs to have "overflow: hidden" in order to clip the overflown content. We set "height: 0" to collapse the box initially.

The transition will take effect on the wrapper's height. When the box needs to be collapsed, we set the wrapper's height to 0. CSS3 transition will smoothly slide up the box. When expanding (sliding down), we set the wrapper's height back to the height of its enclosed content.

$('article').on('click', function() {
  slide($('.content', this)); 
});

function slide(content) {
  var wrapper = content.parent();
  var contentHeight = content.outerHeight(true);
  var wrapperHeight = wrapper.height();

  wrapper.toggleClass('open');
  if (wrapper.hasClass('open')) {
    setTimeout(function() {
      wrapper.addClass('transition').css('height', contentHeight);
    }, 10);
  }
  else {
    setTimeout(function() {
      wrapper.css('height', wrapperHeight);
      setTimeout(function() {
        wrapper.addClass('transition').css('height', 0);
      }, 10);
    }, 10);
  }

  wrapper.one('transitionEnd webkitTransitionEnd transitionend oTransitionEnd msTransitionEnd', function() {
    if(wrapper.hasClass('open')) {
      wrapper.removeClass('transition').css('height', 'auto');
    }
  });
}

The trick is that we don't want to keep a fixed height on the wrapper when it finishes expanding. A fixed height will clip its content when it grows, or leave unnecessary space at the bottom when the content shrinks. To fix that, we need to set height back to "auto" in order to "relax" the height. However, setting "height: auto" on HTML elements with CSS3 transition will make the transition have no effect. We have to remove transitions before setting "height: auto".

Demo and source code

Tested in Chrome, Safari, FireFox, and Opera Mobile Emulator


8 comments:

  1. Why you call this a "CSS3 Tansition"?
    Its JS doing the job here... I was looking for CSS only solutions and thanks to your document title google indicated me here ... :-(

    ReplyDelete
  2. This is about using JS to overcome a limitation in CSS3 transition. The transition itself is still done by CSS3.

    ReplyDelete
  3. where does the javascript go? I have tried putting this into my website however it only shows the "click me to expand" part but when I click it nothing shows up.

    ReplyDelete
  4. Hmm, weird. Works fine for me in Chrome. Try enclose the JavaScript in an immediate function:

    (function($) {

    // Put JavaScript here

    })(jQuery);


    And have this snippet at the end of your page just before the closing BODY tag.

    ReplyDelete
  5. Excellent proof of concept! Indeed works great on Chrome, but after implementing in a project it appeared that IE11 (and less often FF25) sometimes 'skips' the animation. I enlarged the timeouts, this helped a but. I also added the 'transitioning' class during transitioning, so I could prevent possible duplicate events being triggered. This also helped a lot, but it feels too quirky for me to implement in a production site at this moment.

    Thanks a lot for your solution!

    ReplyDelete
  6. How do i handle a link to another url within the toggled content? clicking on the link closes the div.

    thanks, if anyone can help...

    ReplyDelete
  7. $('h2').on('click', function() {
    slide($('.content', this));
    });

    This will trigger the sliding only if you click the title.

    ReplyDelete
  8. Sorry, I forgot to update the context of the selector. What you need is:

    $('h2').on('click', function() {
    slide($('.content', this.parent));
    });

    ReplyDelete