Draggable content. cards. on. the. web. But why?
Good question. Well, for starters, its getting popular on mobile devices. You know, with iOS 13 or 14 (I've lost track), they were introduced to native Apple applications, for example: Apple Music, Stocks etc.
But Altin, it getting popular doesn't mean nich. Remember earth being flat and all ? That was pretty popular.
Fair enough.
Lets look from another perspective.
Good UX-es are often matters of creating solid view hierarchies. Whats important, what is importanter and what is the most importantest... Based on that, you can make informed decisions on what gets shown where.
But what if you get to a stalemate position. What if both views you need to show to the user are the most importantest. In a desktop or tablet, that would be easy. There's enough screen real estate. But this article is about a pattern only seen on mobile devices.
Using buttons to toggle between said views is not going to be very intutive, and as such a lot of users will never discover them.
So now you might understand why Apple choose to have a draggable Now Playing
section sitting at the bottom of their Music app. Or why they have the Business News
section always visible to the user in the Stocks app. Google uses a similar pattern in their Google Maps app too.
The how
Now that we are done with explaining why we might want to use this pattern, lets get our hands dirty and see how difficult it is to implement this on the modern day web.
Step 1 - Start Small
From the get go, we can see that we are going to need 2 cards. The main content one, and the draggable content one. Lets get rolling:
<body>
<div class="main-card"></div>
<div class="draggable-card"></div>
</body>
.main-card {
background-color: var(--red);
width: 100%;
height: 100%;
}
.draggable-card {
background-color: var(--blue);
width: 100%;
height: 100%;
position: fixed;
top: 70%;
min-height: 20vh;
}
That doesn't look anything like draggable card on top of a content. Lets add the thumb and style it up a bit:
...
<div class="draggable-card">
<div class="draggable-thumb-wrapper">
<div class="draggable-thumb"></div>
</div>
</div>
.draggable-card {
...
border-radius: 1rem;
}
.draggable-thumb-wrapper {
display: flex;
align-items: center;
padding: 10px 0px;
cursor: pointer;
}
.draggable-thumb {
margin: auto;
border-radius: 50px;
width: 35px;
height: 4px;
overflow: hidden;
background-color: white;
box-shadow: 0px 0px 0px 1px white;
}
Step 2: Event Listeners.
There are 3 event listeners that we are going to use in order to control the blue card's position in the Y axis. Their naming is pretty self-explanatory. But I'd still rather give a quick intro:
touchstart - fires when touch starts on this particular element.
touchmove - fires when there's a touch event going through said element. It doesn't neccesarily have to start on this particular element. It's enough the touch passes through.
touchend - fires when touch ends on this particular element. Again, doesn't need to have started on this element, its enough it ends here.
<div
class="draggable-thumb-wrapper"
ontouchstart="startDraggingHandler(event)"
ontouchmove="draggingHandler(event)"
ontouchend="stopDraggingHandler(event)"
>
...
</div>
Below the body
tag of our html file, add a <script>
tag with the following contents:
function startDraggingHandler(event) {
const { clientY } = event.touches[0];
}
function draggingHandler(event) {
const { clientY } = event.touches[0];
}
function stopDraggingHandler(event) {
const { clientY } = event.changedTouches[0];
}
clientY
tells us exactly where the touch event is occuring/has occured, relative to viewports height.
As of now, the draggable part is still not draggable, but we've laid out a very good foundation.
Step 3: Putting the drag in draggable
We are going to use transform
CSS property to drag the blue card up and down the Y axis. We could also do this using the top
property, but that would look very glitchy since calculations and layouts would happen in main thread. Transform
on the other hand, offloads its work to the GPU thread, guaranteeing us the utmost main thread performance.
Because of that, we need to introduce a variable in our JS code which will hold the transform
value.
let yDragValue = 0;
...
Remember when I talked about touchmove
and touchend
I mentioned that they get fired regardless of where the touch started. To take care of that we need another variable.
let yDragValue = 0;
let isDragging = false;
...
Last but not least, we need to keep track of where the touch started, which we can then use to calculate how much the card should move up.
let yDragValue = 0;
let isDragging = false;
let initialDragClientY = 0;
...
Lets now put these variables to use:
let yDragValue = 0;
let isDragging = false;
let initialDragClientY = 0;
function startDraggingHandler(event) {
isDragging = true;
const { clientY } = event.touches[0];
initialDragClientY = clientY;
}
function draggingHandler(event) {
if(!isDragging) return;
const { clientY } = event.touches[0];
yDragValue = clientY - initialDragClientY;
}
function stopDraggingHandler(event) {
if(!isDragging) return;
const { clientY } = event.changedTouches[0];
isDragging = false;
}
Okay, we got all the things we need. We now need to update the position of the element by using the transform
CSS property.
...
const draggableElement = document.querySelector('.draggable-card');
...
function draggingHandler(event) {
...
draggableElement.style.transform = `translateY(${yDragValue}px)`;
}
Lets try it out:
It feels a bit too riggid though. Lets ease it out a bit:
.draggable-card {
...
transition: transform 0.2s ease-out;
}
Setting will-change
to transform
in our draggable card element, will further enhance the performance.
Wrapping it up
There is still a lot of stuff to be done here. But these all fall under progressive enhancments. I am referring to things like: snap in position, cancel the drag, handling the yDragValue when the dragging starts at the top, after its been dragged from the bottom.
For all of these, you can check out my github repo, where I've done something similar to the Stocks iOS app with all the snapping and dragging taken care of. Its vanilla javascript and pretty easy to read too.
All things considered, the experience is pretty decent. There's one thing I would wish to add to this implementation, and that is a spring type animation. But that's a bit harder to achieve in vanilla JS. If you are using React, you can use libraries like react-spring
or framer-motion
to achieve that.
Top comments (1)
Why have we destructured clientY here in stopDraggingHandler?