Most of us appreciate applications that pair good UI design with a “WOW factor” that enriches our experience, right?
So, when a friend of mine showed me Tinder on his phone, I noticed the swipe feature and immediately recognized a bunch of ways to use this touch-based interaction. With the swipe of his finger, he could choose three different actions; it was totally intuitive.
With the meteoric rise in the use of touch screens on mobile devices, glamorous touch gestures have become a standard for interacting with applications.
On Tinder, a profile is displayed on a bunch of stacked cards that can be swiped to the left, to the right, or to the top of the screen. Each direction triggers a different action; swiping right, for example, means we like someone’s profile on the app.
So, inspired by Tinder, I decided to build a component that can be used for a number of things. Tinder has done an excellent job of creating a component for online dating and hookups, but I wanted to create one that can be used more widely by all kinds of applications. So, my solution is going to take reusability into account.
I started out installing Tinder on my smartphone and trying it out myself… you know, for the sake of research.
Next, I noticed that there was very little information about how to build a swipeable stacked card component with smooth performance on all devices. And, if you develop mobile applications, you must be familiar with the pain involved in achieving a standard that works for all devices, right? Ouch!
This discovery showed me that I was on the right track. The world needs more swipeable cards, and it was up to me to pick up the slack. So, it was time to do what I always recommend for building component. Before anything else, stop, analyze, and consider the largest number of scenarios. Think about where and how you can simplify and only then start developing. You’ll save a lot of time and effort later on.
Let’s look at a potential way to use my component. The example shows a vacation planning app called “What Kind Of Traveler Are You?” Categories such as “Art and Culture,” “City Breaks,” and “Adventure and Outdoor” are displayed on a single card, and the cards are stacked.
To dismiss (“Unlike”) a vacation category, you swipe left. A swipe to the top bookmarks the category to your favorites. And, when you like a destination? Just swipe right.
Inside the Component: The HTML Structure
Before we start building the component, we need a basic HTML structure to work with. I always use a wrapper to contain the elements of the component, such as cards and the elements for each overlay. When we touch the screen inside the component and then swipe, an overlay will appear over the card showing the action we chose.
Five Configurable Options for Component Reusability
After analyzing the component, I chose five configurable options to allow for good interaction and use. (But if you just want to get the component running smoothly by adding your content, you can stick to the default options.)
Organizing the Elements of the Component
Achieving the proper interactions requires a lot of calculations based on the device screen. For that, we need to be sure that the DOM is fully loaded.
I added an EventListener to the HTML named DOMContentLoaded. This event is fired when the HTML document has been completely loaded and parsed with stylesheets, images, and HTML structure.
All the code that will be developed must be inside this EventListener:
I created a CSS class called ”init” that is added to the component to keep everything hidden until the DOM is fully loaded. Because we previously added ”DOMContentLoaded,” we know exactly when we can remove this class. When we have organized all the elements, we then assign the necessary classes and the calculations that we need for the component. After that, we can remove the ‘init’ class.
The cards have a default style and size; I have defined that the first card determines the size of all the other cards. For this example, I set the card size, and this can be changed in the CSS:
At this stage, we should know how many elements there are for the component to organize. So now we count the elements and set the active element of the stack. Because they will be reusable, all actions are isolated.
At this stage, the settings we defined will influence the organization on the screen. The StackedOptions (Bottom, Top, or None) and the margin that were added (elementsMargin) will enter the accounts of the organization. We chose the “Top” view, and we will add the class “stackedcards-origin-top” that defines the transform-origin (to change the position of transformed elements) of the elements on the component. As you can see in the code, the margin is defined as 10 (elementsMargin = 10). This margin is added to all the elements, except the first one, so that you can see the cards that are behind it. Take a look at the image and code:
The variable “elTrans” was created to solve the problem of positioning the elements when we have the top set in stackedOptions, because it is added to the class “stackedcards-origin-top” as it is done for the bottom view, but it is not enough. This class changes the transform-origin of the elements, but in terms of the organization of the elements, we have to do a little more. This variable will calculate the margin value * the number of elements of the component minus the first. In other words, it will create a small margin to create the stacked effect which in this case will be:
What will influence this variable? It will only give an inline CSS style margin-bottom to the element in the DOM that holds all the cards (“stackedcards-container”) with the calculated value in the elTrans variable.
Next, we add the class “stackedcards-active” and position the elements that are not visible:
At this stage, if the overlays are visible, we will position them and add the respective classes. If they are hidden, the elements only receive the class “stackedcards-overlay-hidden”:
Now that we have organized all the elements and assigned the classes, we won't be needing the “init” class, so it can be removed:
Interacting With the Component: Implementing the Touch Events
This is all amazing, but it won’t work if we can’t interact with the component. This leads us to the following question: How are we going to interact with the component on our device?
I chose to go with touch events.
So, for interactivity, we will use:
- touchStart: Defines the moment we start touching the element.
- touchMove: Defines when we touch and move a touch point across the surface of our screen, so we know where it is on our screen.
- touchEnd: Defines when we stop touching the element.
Let's define the variables we need for touch events and add the EventListeners to the element:
Using Overlays for Smooth Transitions
Moving multiple elements at the same time on mobile devices raises many performance concerns. We must always move two overlays simultaneously on the screen to create a fade out from one overlay while also fading into the next overlay.
For smooth as butter transitions, I created a setOverlayOpacity () function. This function calculates the opacity level for each swipe movement from any direction of the screen (left, right, or top). It is based on translateX and translateY as well as on the element’s height:
Optimizing the Component: Tidying Up the HTML and CSS Structure
Although we planned the development phase, sometimes, along the way, we discover additional requirements that weren't planned for. So, we add and remove CSS classes, and sometimes we also have to change the initial HTML structure.
Despite this, it’s essential that we always aim to simplify the CSS classes that we are creating and implementing; this way, we end up with CSS that is easy to understand and maintain.
The final HTML structure:
The default CSS architecture:
Let’s Tap: Global Actions for an Alternative to Swiping
Tinder also has buttons as an alternative to the swipe. Because these are useful, I decided to include them in the component. For each button, I included actions (Left, Right, and Top) so we only need to add an EventListener to a card and identify the action we want:
And add this snippet to your html:
Okay, at this point we have all the elements we need, so now what?
Planning for Smooth Transitions
Good performance is always a concern when working with mobile devices, so I optimize to the max when developing a new component.
The requestAnimationFrame warns the browser that there will be a change. The browser and the screen are then prepared for the changes that will be made with our animations. When we call requestAnimationFrame repeatedly to create an animation, we guarantee that our animation code is called only when the device is ready to change the screen. The result is a smooth and effortless transition.
The same goes for the will-change property. This property tells the browser what type of changes will affect the element, so the device can set up appropriate optimizations before the element is changed. We have better performance and smoother animations without any leaks or screen flips.
Leaving Decisions for the Future
When developing components for reuse, we always have to make decisions.
These decisions are made based on the experience we want to provide our users and the vision that we have for the component. We are left with a question: “How far should our decisions go?”
During the development of this component, I made decisions such as when to send a card out and what to do with it, the size of each card in the stack, animation speeds, type of animations, etc.
These decisions take into account the need to provide a good default solution that is reusable at the same time. For example, when we send the card off the screen, it goes to position 1000; the element is still there, only it's hidden. Now you might ask?
“Why didn’t you just remove it from the DOM?”
As I explained earlier, this component has a wide variety of uses. If we remove elements in the DOM, we require repaints, reflows, or both on our screen. This impacts the performance of the mobile device, as well as our animations. Because of these implications, I don’t remove any elements in the DOM. Instead, I leave it to the developer to decide what is best for their particular case. For them, I’ve created a method that can be used to remove DOM elements after a swipe: