Animating an Accordion in Salesforce Lightning

Jon Balza
8 min readDec 5, 2018

--

Photo by Lex Aliviado on Unsplash

So… let’s talk about building an Accordion in Salesforce Lighting/Aura. Before you close your tab and after you think, “WHY? Salesforce has a set of accordion components already! Tell me something I don’t know!” Let me answer your first question: “Why.”

So, normally i would would agree, its better to use an OOTB component, but the Lightning Accordion is a particular pain in the butt. First-off, you always need to use <lightning:accordion /> together with <lightning:accordionSection />. This is how you control open states of accordions. You need to use the activeSectionName attribute on <lightning:accordion /> where you’ve passed a single value (or array of values!) which has to match up with the name attribute on the <lightning:accordionSection /> , which will then open the accordions you need. This gets to be particularly difficult when using a list of data (like a list of Salesforce records) to display a dynamic set of accordions. You’re also limited to the default attributes, so you can’t integrate a sub-title or a tooltip, both of which are common.

So, occasionally for our clients at 7Summits, we end up writing our own <c:Accordion /> as a Lightning component. In addition to solving the implementation difficulty issues, it also gives us the chance to customize and enhance the design, animation, and overall UI of the interface. I’d like to show you a really simple version of an Accordion in order to get you started creating your own.

A Basic Accordions, With Just Show/Hide

Similarly to my previous Modal component, we can start with a basic “show/hide” layout. This uses a single isOpen attribute, and shows/hides the content of the Accordion (by adding the .slds-is-open class based onthat attribute). Simple and clean, but leaves something to be desired from a UI perspective. It provides us with a great start, however. So let’s put that into motion. We’ll use basic SLDS styling to get started for this one.

<!-- Accordion.cmp -->
<aura:component description="Accordion">
<aura:attribute name="title" type="String" default="Title" />
<aura:attribute name="isOpen" type="Boolean" default="false" />
<div class="slds-accordion__list-item">
<section class="{!'slds-accordion__section '+ if(v.isOpen, 'slds-is-open', '')}">
<div class="slds-accordion__summary">
<h2 class="slds-accordion__summary-heading slds-text-heading_small">
<button onclick="{!c.handleClick}" class="slds-button_reset">
{!v.title}
</button>
</h2>
</div>
<div class="slds-accordion__content">
{!v.body}
</div>
</section>
</div>
</aura:component>

Now that’s about as basic an accordion as you can get. There’s a lot that would needed to be added, even from an HTML perspective. Please don’t forget about accessibility (aria- attributes, for instance), icons for usability, and any custom styling.

With this type of setup, the controller method is quite simple:

/* AccordionController.js */
({
handleClick: function(component, event, helper) {
var isOpen = component.get("v.isOpen");
component.set("v.isOpen", !isOpen);
}
})

Here, we’re just taking the onclick handler of the button, and toggling the isOpen attribute of the accordion, which shows or hides its body. At this point, you can add the correct implements attribute to <aura:component> and you have a usable component.

Animating the Height of the Accordion

OK, before we get to the more complex work in the Accordion component, let’s talk animation. When creating an animation in a browser, it’s best to try and use CSS to handle it. Usually, this consists of a transition statement, in an example such as this:

/* Accordion.css */
.THIS .slds-accordion__content {
/* Some visibility/opacity styling to override the default SLDS styling */
visibility: visible;
opacity: 1;
height: 0;
transition: height .5s ease-in-out;
}
.THIS .slds-is-open > .slds-accordion__summary {
margin-bottom: 0;
}
.THIS .slds-is-open > .slds-accordion__content {
overflow: hidden;
height: 200px;
}

That is simple, and works great, however, we have an issue — we don’t know the height of the .slds-accordion__content element when it’s open!

A generic accordion (or similar component) is going to need to be used in a wide variety of use-cases, and may have a varying amount of content in the body. It may be a single sentence, or an expanded image, or a full few paragraphs. Even in what may be its most common application, a FAQ list, it’s going to have a lot of different heights to deal with.

There are a TON of CSS-only techniques that try and solve this with a variety of different tricks, from animating the max-height, to just leaving any gaps create, or more. They all have downsides, however – from unavoidable delays after closing the modal, to unrefined layouts. Really, it’s just best to use JS to animate the height. So we need to set the height of our accordion content, based on the DOM element.

So, here’s where we’re at:

  1. We want to animate height of the .slds-accordion__content element with CSS.
  2. We won’t know the height of that element, so we can’t set it in our CSS file, and it needs to be done with JS.
  3. We’ll need to get the height of the element within Salesforce Lightning’s framework.

Render events in Salesforce Lightning

So the Salesforce documentation talks a little bit about modifying DOM elements within the Aura/Lightning framework. It’s quite helpful, and the technique we’re going to need to use. We need to look at using the render event on the component. Here’s the problem we’re avoiding with this issue:

If we try and write a onInit handler, like this:

<!-- Accordion.cmp -->
...
<aura:handler name="init" value="{!this}" action="{!c.onInit}" />
...
<div class="slds-accordion__content" aura:id="accordionContent">
...
</div>
...

… and the method that handles it like this:

/* AccordionController.cmp */
onInit: function(component, event, helper) {
var contentHeight = component.find("accordionContent").getElement().offsetHeight;
console.log(contentHeight);
}

You’re going to run into a problem. Chances are, your component won’t show on the page, and it’s because the init event doesn’t guarantee that the DOM elements are going to be in place yet. Instead, you need to use that render event handler to get the height of an element reliably, as follows:

<!-- Accordion.cmp -->
...
<aura:handler name="render" value="{!this}" action="{!c.onRender}" />
...
<div class="slds-accordion__content" aura:id="accordionContent">
...
</div>
...

Controller:

/* AccordionController.js */
onRender: function(component, event, helper) {
var contentHeight = component.find("accordionContent").getElement().offsetHeight;
console.log(contentHeight);
}

Now, your console.log() statement won’t error out, and you can use this information along with a bit of Lightning/CSS trickery to get your animation working properly. The render event will render any time the DOM changes in your component or your Lightning Component needs to re-render. This means that we can keep this onRender handler, and use it in tandem with the handleClick method we created before, so that every time the isOpen state is toggled (or another change is made), the onRender method will re-run. That’s really useful, and exactly what we want moving forward.

Now we know how to get the height of the element, when it is open. The next issue we need to solve is getting the height in its closed state. To do that, we can write a helper method. We’ll call it getHeight, of course, and we’ll want to pass it the DOM element that we need to find the height of. (Basically, the component.find("accordionContent").getElement() statement from above.)

/* AccordionHelper.js */
({
getHeight: function (element) {
element.style.display = 'block'; // Make it visible
var height = element.scrollHeight + 'px'; // Get it’s height
element.style.display = ''; // Set it back!
return height;
}
})

Essentially, what’s happening here is that we need to force the element to be shown so we can properly get the element’s height, then quickly hide it again before it’s shown to the user. Then, we can return the height that we just calculated.

Putting it all together

OK, now we just need to assemble this all a bit.

First, let’s update our controller onRender method to use slideDown and slideUp methods that we’ll create in our helper:

/* AccordionController.cmp */
onRender: function(component, event, helper) {
var isOpen = component.get("v.isOpen");
if (isOpen) {
helper.slideDown(component, event, helper);
} else {
helper.slideUp(component, event, helper);
}
}

Most of the following technique is based on some of the logic in my previous Animating Modals in Salesforce Lightning blog post, specifically where we use a closed > opening > open > closing > closed cycle to control the different states. Let’s adjust our CSS to account for those states, and add a basic half-second CSS animation:

/* Accordion.css */
.THIS .slds-accordion__content {
visibility: visible;
opacity: 1;
display: block;
height: 0;
transition: height .5s ease-in-out;
}
.THIS .slds-accordion__summary {
margin-bottom: 0;
}
.THIS .accordion-state_opening > .slds-accordion__content,
.THIS .accordion-state_closing > .slds-accordion__content {
overflow: hidden;
}
.THIS .accordion-state_open > .slds-accordion__content {
/* Note we don’t add in the "open" state height! */
height: auto;
}
.THIS .accordion-state_closed > .slds-accordion__content {
display: none;
}

Now, let’s walk through the slideDown helper method:

/* AccordionHelper.js */
({
...
// Shows an element
slideDown: function (component, event, helper) {
var accordionElement = component.find("accordionContent").getElement();
var isOpenState = component.get("v.isOpenState");
// Get the natural height of the element
if(accordionElement && isOpenState === 'closed') {
var height = helper.getHeight(accordionElement); // Get the natural height
accordionElement.style.height = height; // Update the height
component.set("v.isOpenState", 'opening');
// Once the transition is complete, remove the inline height so the content can scale responsively
setTimeout($A.getCallback(function () {
accordionElement.style.height = '';
component.set("v.isOpenState", 'open);
}), 500);
}
},
...

The slideDown method first finds a reference to the aura:id="accordionContent" element in the component, then gets the actual DOM element using .getElement(). We need the actual DOM element to both find the height of the element, and add that directly to an inline style.

(Occasionally, you may need to adjust the var accordionElement line if you’re getting errors that looks something like “Cannot getElement() of undefined.” This adjustment could look something like the following:)

var accordion = component.find(“accordionContent”);
if(accordion) {
var accordionElement = accordion.getElement();
}

Also, we’re going to be using slideDown in the render event handler, which will fire every time the component changes, which means we need to add a bit of logic to only run the code when we need it to, in order to avoid any looping or recursion. That’s where the if(isOpenState === 'closed') comes in. The if(accordionElement) is there just in case the render event can’t find that element for some reason.

We use our getHeight helper class to get the height of the element, and then set an inline style of that retrieved height. Then, we can set the isOpenState to opening, and wait half a second to set the isOpenState to open, while the CSS animation does it’s work.

Note also, that the setTimeout also uses $A.getCallback(), which is important so that Lightning doesn’t lose track of any changes we’re making to the component in the setTimeout.

The slideUp (or hide) method works very similarly, it’s as below:

/* AccordionHelper.js */
({
...
// Hide an element
slideUp: function(component, event, helper) {
var accordionElement = component.find(“accordionContent”).getElement();
var isOpenState = component.get(“v.isOpenState”);
if(accordionElement && isOpenState === ‘open’) {
// Give the element a height to change from (we could also use getHeight() here)
accordionElement.style.height = accordionElement.scrollHeight + ‘px’;
// Set the height back to 0
setTimeout($A.getCallback(function () {
component.set(“v.isOpenState”, ‘closing’);
accordionElement.style.height = ‘0’;
}), 10);
// When the transition is complete, hide it
setTimeout($A.getCallback(function () {
component.set(“v.isOpenState”, ‘closed’);
accordionElement.style.height = ‘’;
}), 500);
}
}
})
```

I’ve used a bit of setTimeout silliness to properly get the animation to work, I’m sure it can use a bit of optimization, but for now this triggers the correct CSS animation states.

You can see the complete example on my Github:

https://github.com/jonbalza/lightning-accordion-example

Thanks for reading, I hope you find this useful. Please write a response with any suggestions or comments you may have.

--

--

Jon Balza

Web Designer & UI Developer at 7Summits specializing in Salesforce Community Cloud-led solutions. • 5+ Salesforce certifications • jonbalza.com