Animating Modals in Salesforce Lightning

Jon Balza
8 min readNov 23, 2018

--

Users of today’s apps and websites expect a lot from their user interfaces. Gone are the days where a basic design and the ability to complete a task were enough to hook a user on your app. Now, free apps are a dime a dozen, and competition for a space on your user’s home screen or browser is intense. In order for your Salesforce Community or App to meet your user’s baseline expectations, you need to hold up against all the other apps they use day-in and day-out.

In today’s blog post (and future posts), I hope to share a lot of the techniques that we use at 7Summits to provide some of these enhanced user experiences using Salesforce’s Lightning Experience and Community Cloud. We’ll start with an example modal animation.

First, the Easy Way — a Basic On/Off Modal

The easiest, and probably most common method of showing/hiding any component in Lighting is using an attribute to show/hide the HTML that renders that component. In this case, for our foundational modal, we can use an isOpen Boolean attribute that component can use to show/hide the modal quickly. It kind of looks like this:

A basic on-off modal implemented in Salesforce Lightning, with no supporting animation.

Let’s set it up. Below is our basic modal component — note the isOpen attribute, and some basic HTML that frames out the modal, container, and backdrop behind the modal. We’re also passing title and body attributes, and registering an onclose handler so we can quickly control the isOpen toggle in whatever parent component we use.

<!-- Modal.cmp -->
<aura:component description="Modal">
<aura:attribute name="isOpen" type="Boolean" default="false" />
<aura:attribute name="title" type="String" />
<aura:registerEvent name="onclose" type="c:Modal_Event" /> <aura:if isTrue="{!v.isOpen}">
<div class="modal_container slds-align_absolute-center">
<div class="modal_backdrop"></div>
<article class="modal">
<header class="modal_header">
<h1>{!v.title}</h1>
<a class="modal_close" onclick="{!c.closeModal}"><lightning:icon iconName="utility:close" size="xx-small" alternativeText="Close Modal" /></a>
</header>
<div class="modal_body">
{!v.body}
</div>
</article>
</div>
</aura:if>
</aura:component>

So now we just need a short controller method to handle that onclick attribute, which fires the onclose event:

/* ModalController.js */
({
closeModal: function(component, event, helper) {
let closeEvent = component.getEvent('onclose');
closeEvent.setParams({
type: 'close'
});
closeEvent.fire();
}
})

That’s pretty much it, with the exception of some styling. Let’s try this:

.THIS {
z-index: 1000;
}
.THIS,
.THIS .modal_backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.THIS .modal_backdrop {
background: rgba(220,220,220,.9);
}
.THIS .modal {
width: 100%;
max-width: 420px;
background: #fff;
border-radius: 10px;
box-shadow: 0 10px 10px -5px rgba(0,0,0,.1);
position: relative;
margin: auto;
}
.THIS .modal_header {
background: aliceblue;
padding: 15px 40px 15px 20px;
position: relative;
border-radius: 10px 10px 0 0;
}
.THIS .modal_body {
padding: 20px;
}
.THIS .modal_close {
position: absolute;
right: 20px;
top: 15px;
}

You can view the entire Basic Modal component on my GitHub.

Now, our parent component uses an onclose handler to set the isOpen attribute to false. This hides the modal by doing something like the following:

<!-- Parent.cmp -->
<div>
<p>
<lightning:button type="button" variant="brand" onclick="{!c.openModal}" label="Open Basic Modal" />
</p>
<c:Modal_Basic isOpen="{!v.isOpen}" onclose="{!c.closeModal}" title="Basic Modal">
<p>This is a basic modal.</p>
</c:Modal_Basic>
</div>

You could handle the c.openModal and c.closeModal controller functions like so:

/* ParentController.js */
({
openModal: function(component, event, helper) {
component.set("v.isOpen", true);
},
closeModal: function(component, event, helper) {
component.set("v.isOpen", false);
}
})

Well, that’s all we need for a basic modal. (By the way, the code referenced in our Parent component and controller won’t change for the rest of this post.)

Next, let’s add a transitional state using CSS animation.

A First Attempt At Adding In CSS Transitions

So, we have the basics, and, with a single isOpen attribute, it’s very easy to deal with opening and closing the modal, which is awesome. Let’s keep the that single isOpen attribute. However, our modal is lacking a little in the UX/UI department. To start, let’s try and add some animation when opening with CSS.

An improved animated modal in Lightning making use of CSS animations

Component Additions

Let’s go back to our Modal.cmp component and make a single modification by adding an openClass attribute. This is a hook for our CSS animation.

<!-- Modal.cmp -->
<aura:component description="Modal - Basic Functionality">
<aura:attribute name="isOpen" type="Boolean" default="false" />
<aura:attribute name="title" type="String" />
<aura:attribute name="openClass" type="String" default="" />
<aura:registerEvent name="onclose" type="c:Modal_Event" />
<aura:handler name="change" value="{!v.isOpen}" action="{!c.handleIsOpenToggle}" />
<aura:if isTrue="{!v.isOpen}">
<div class="{!'modal_container slds-align_absolute-center ' + v.class + ' ' + v.openClass}">
<div class="modal_backdrop"></div>
<article class="modal">
...
</article>
</div>
</aura:if>
</aura:component>

With a dynamic class created with openClass, we can use that as a hook for our CSS animations. The base state of the class tied to the isOpen property, and we can toggle it by creating the change handler for v.isOpen. That handler calls a c.handleIsOpenToggle method in our controller that does the work of actually adding the class. A simple way to do it is like as follows:

/* ModalController.js */
({
handleIsOpenToggle: function(component, event, helper) {
let isOpen = component.get("v.isOpen");
if (isOpen) {
component.set("v.openClass", 'is-open');
} else {
component.set("v.openClass", '');
}
},
...
})

CSS for the Animation

So, now successfully adding a class to our modal based on the isOpen state. Now, let’s try to add the CSS animation. Here, I’ve only showed the changed lines:

.THIS .modal_backdrop {
...
transition: opacity .25s linear;
}
.THIS.is-open .modal_backdrop {
opacity: 1;
}
.THIS .modal {
...
transform: translateY(-20px);
opacity: 0;
transition: transform .5s linear, opacity .5s linear;
}
.THIS.is-open .modal {
transform: translateY(0px);
opacity: 1;
}

This is the CSS we need for now, which adds an animated state on both the backdrop and the modal itself.

However, if you try this out as is, there’s a problem: there is still no visible animation. That’s because the openClass is added immediately upon the toggle of the state, which timed exactly to when the modal HTML is added to the component. That means no animation, because the modal is never displayed on the page without the is-open class. We can fix that with very short setTimeout JS function. Let’s add that to our controller.

handleIsOpenToggle: function(component, event, helper) {
const isOpen = component.get("v.isOpen");
if (isOpen) {
setTimeout($A.getCallback(function() {
component.set("v.openClass", 'is-open');
}), 10);
} else {
setTimeout($A.getCallback(function() {
component.set("v.openClass", '');
}), 10);
}
},

The setTimeout is a pretty standard JS function. Note that we also add a $A.getCallback to the callback so that the Lightning framework can properly rerender the component.

Now, if you make the updates to the modal, you’ll see the animation when the modal opens. However, we now have a slightly different issue than before when we close the modal — the isOpen toggle removes the HTML for the modal before the CSS animation can even take place. We’re going to need to adjust our technique to better handle this type of closing animation.

Finally, the Elegant Way — Expanding to Four Fluid States of the States of the Component

Ok, so the two single states that the boolean isOpen provides us doesn’t appear to be enough. Let’s think about this a bit and try a slightly different approach. Really, an opening and closing modal goes through a bit of a cycle, that consists of four basic states, as shown below:

The 4 states: Closed > Opening > Open > Closing

The single isOpen toggle works, and makes handling the state of the modal in a parent component very easy, so we don’t want to change that. We need to expand the modal a bit to handle these 4 states, however. This won’t be too difficult, as we have laid some of the groundwork with our previous attempt with the setTimeout function.

The final example for the animated Lightning modal.

Here we go. First thing to do is get rid of the openClass attribute. We’ll handle the creation of the class based off of a couple of new attributes we’ll add:

<!-- Modal.cmp -->
...
<aura:attribute name="openState" type="String" default="closed" />
<aura:attribute name="openDelay" type="Integer" default="500" />
...

We’ll use openState instead of openClass, and set it to a default of closed, which lines up with our default isOpen value of false. I’ve also added an Integer openDelay, and defaulted it to 500, which we’ll use to define the length in milliseconds of the closing and opening transition states.

Next, in our component, instead of removing the HTML for the modal when isOpen is false, we remove it when the openState is closed. It’s a quick change to start the modal with a line like this:

<!-- Modal.cmp -->
<aura:if isTrue="{!v.openState != 'closed'}">
<div class="{!'modal_container slds-align_absolute-center modal-' + v.openState}">
...
</div>
</aura:if>

Now, let’s adjust our change handler to cycle through the states after our defined delay, so our new controller looks a bit like this:

handleIsOpenToggle: function(component, event, helper) {
const isOpen = component.get('v.isOpen');
const openDelay = component.get('v.openDelay');
if (isOpen) {
component.set('v.openState', 'opening');
setTimeout($A.getCallback(function() {
component.set('v.openState', 'open');
}), 1);
} else {
component.set('v.openState', 'closing');
setTimeout($A.getCallback(function() {
component.set('v.openState', 'closed');
}), openDelay);
}
},

So, our toggle takes a look at the isOpen value, and cycles through the appropriate opening/closing states as needed. The closed and closing states now work perfectly, and is an exact representation of our cycles. The opening and open states have a bit of oddity to them — note the 1 value for the setTimeout call, instead of using openDelay. This goes back to the same reasons as above, where the CSS animation needs a before and an after state to show the transition. Speaking of which, here’s our revised CSS:

.THIS .modal_backdrop {
...
transition: opacity .25s linear;
}
.THIS.modal-opening .modal_backdrop,
.THIS.modal-closing .modal_backdrop {
opacity: 0;
}
.THIS.modal-open .modal_backdrop {
opacity: 1;
}
.THIS .modal {
...
transform: translateY(-20px);
opacity: 0;
transition: transform .5s linear, opacity .5s linear;
}
.THIS.modal-opening .modal {
transform: translateY(-20px);
opacity: 0;
}
.THIS.modal-open .modal {
transform: translateY(0px);
opacity: 1;
}
.THIS.modal-closing .modal {
transform: translateY(20px);
opacity: 0;
}

That should do it! Note we haven’t done anything fancy with renderers yet in Lightning, that’s because we’re not doing any direct DOM manipulation. In my next post, we’ll expand on this technique a bit further, using the openStates technique along with a render handler on an accordion, so we can animate the height of the accordion appropriately.

Even so, there’s a variety of improvements and adjustments that could still be made, from styling, to z-index fixes. Also, one quick thing to note — potentially, this basic modal setup can cause some z-index issues with other components on the page, but I’ll address global modals in a future blog post. We can address all of that in future blog posts — for now, view all the code along with an example here:

--

--

Jon Balza

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