Bootstrap’s implementation of Collapse

Sachin Singh
5 min readMay 14, 2019

Disclaimer: We are about to spill some well known secrets!

As a developer I always wondered how expand/collapse animation worked in libraries like bootstrap and jQuery. They were so seamless and yet so hard to replicate when you try them on your own. I always used to mess up the expand animation due to the fact that it’s impossible to calculate the target height of hidden element. I knew that unless I know the target height of the element, I cannot make the animation work. Collapse animation was easy as the element was visible and target height has to be 0. Long story short, it was messy!

There’s a Google’s post that talks about css “clip” and “clip-path” properties and their significance in terms of animation performance. It’s a good read and you should definitely check it out. In this article we will explore Bootstrap’s expand/collapse functionality.

Let’s get started…

Before we jump into implementation, first let’s understand what we are really trying to solve here.

  1. We don’t want to set fixed height to collapsed element
    Think about it! Does bootstrap asks you to set a fixed height to your elements?
  2. The animation should run in either state (expand or collapse)
    When the element is collapsed, the height is 0. When it’s expanded, it takes its own height. This should not impact the animation.
  3. Functionality should still work in cases where someone overrides the transition css property
    If there are no transitions, transitionend event does not work. What if you are relying on this event to update something after the transition ends? Think about it!

How to calculate height of collapsed element?

This is the most important question, and the most puzzling one. This is why you’re asked to set a fixed height to an element. Let’s see how bootstrap does it:

Bootstrap provides three classes which are kind of important for expand/collapse animation to work: collapse, collapsing, and show.

By default the element has collapse class. This is the base class which serves as the target for animation. We have a collapsing class which is added when actual transition is in effect. If you check the css code, you will realize that collapsing class adds transition property for height. Then we have a show class, which basically turns display: none to display: block.

This is how animation state change occurs when element is expanded:

collapsecollapsingcollapse show

CSS for collapse:

.collapse:not(.show) {
display: none;
}

We know one thing: Transitions do not work on display property. It’s safe to assume that collapse and show doesn’t play any role in terms of animations. Their only role is to show or hide elements after transition is complete. It’s collapsing that does all the work. The question is how?

Let’s do some decoding:

.collapsing {
position: relative;
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}

When transition effect is initiated, bootstrap JS switches collapse class with collapsing class, which changes display: none to height: 0 immediately (as you can see in the code). We still don’t know the actual height of the element yet, but we know that transition can work on height. We just need to figure out actual height of the element, and that’s all we need to transit the state.

This is how bootstrap does it:

var height = element.scrollHeight;
$(element).css('height', `${height}px`);
$(element).on('transitionend', () => {
$(element).addClass('collapse show').css('height', '');
});

When transition ends, all we need to do is listen to the event and add collapse and show classes as shown in code above. Did we just solve our first problem? Indeed, we did!

Now solving collapse should be easy right?

You must be wondering why we reset the height (on transition end) when we know that we could have used that for collapse transition. Well! think about it. What if it was other way around. The elements were already expanded and now we had to manually set the height for each element beforehand. What if there was a smarter way to do that. The bootstrap’s way:

Bootstrap collapse transition goes like this:

collapse showcollapsingcollapse

All we need to do is, remove collapse and show classes, calculate the height of the target element and set it inline, then add back collapsing class to take advantage of its height: 0 property to add the effect.

Do you think it will work?

Not yet! Collapsing class will have no effect due to its specificity being lower than inline css height. We would need to unset the height property to run the animation.

This is what we need to do:

$(element).removeClass('collapse show');
$(element).css('height', `${$(element).outerHeight()}px`);
$(element).addClass('collapsing');
$(element).css('height', '');
$(element).on('transitionend', () => {
$(element).addClass('collapse');
});

This is how bootstrap does it (with little code changes here and there of course!). They also do something very clever!

Before reading any further, why don’t you go back and give this implementation a try first?

.

.

.

Have you given a try? No?

.

.

.

Have you now? Did it work?

I bet it didn’t. What do you think we missed?

Let’s talk about the clever part (some more secret spilling). If you go through bootstrap’s source code, you’ll find a statement similar to one shown below:

$(element).removeClass('collapse show');
$(element).css('height', `${$(element).outerHeight()}px`);
Util.reflow(element); // Bootstrap
$(element).addClass('collapsing');
$(element).css('height', '');
$(element).on('transitionend', () => {
$(element).addClass('collapse');
});

Let’s see what this function does:

Util.reflow = function (element) {
return element.offsetHeight;
};

What? Are you serious? It just returns the element’s offset height and that’s all? Where are we using this property in our code? I mean literally where?

Yes! yes! yes! I had that same reaction. The truth is, it does something very important, quite subtle, and invisible to naked eye.

It causes a layout reflow! Apparently, it’s very important for our collapse transition to work. Here’s the explanation:

A reflow is process in which a browser re-computes page layout. It is triggered whenever a visual change occurs. It can be a change in width or height, or may be element’s position. Browser’s are very smart in detecting these changes and try to optimize (or reduce) reflows wherever possible. For example, if height of an element is set to 100px, which is already taking 100px height, a reflow will not be triggered. This is what happens in our case. We added inline height and immediately removed it. The browser ignored this change, and when it was time to run animations, we never had the starting height for transition. As a result we deliberately had to cause reflow to register element’s initial height before the actual transition could take place.

Now try it again. I bet! Animation will work now. 👍

Moving on to the last part!

How do we ensure that transitionend is still triggered even if animation doesn’t take place? The answer lies in setTimeout.

There you go! All secrets spilled! Well almost!

I still don’t know why accessing offsetHeight caused a layout reflow. May be it’s a hack. May be there’s an explanation. Let me know in the comments below.

Happy coding!

--

--