How to Create the iPhone Interface with Long Press in Javascript
📣 Sponsor
Shaking icons and long presses have become something we are very familiar with from our phone screens. On iPhones in particular, shaking icons usually implies they are draggable and editable - while long presses have become the normal way to get additional options.
In this tutorial, we'll be looking at recreating these effects in Javascript and CSS. In this tutorial, we will cover:
- New CSS features - such as background blurs, and animated flex boxes.
- Long pressing - how to create the long press effect with Javascript.
- Dragging - how to create a simple drag and drop system in Javascript.
Demo
As always, let's start with the demo. This is what we're planning to create today.
- If you click and press on an icon for one second, a popup will flash up.
- If you click and press for two seconds in total, the icons will begin to shake - just like on iphone. This will also work on mobile.
Continue to hold!
Continue to HOLD for another second to cause the icons to shake!
Long Press on the Icons!
Click on an icon and HOLD for 1 second to see a drop down.
Dummy Icons via Aaron Humphreys
Step 1: HTML
In this tutorial, I will not focus on the HTML too much as it is quite rudimentary - suffice to say the HTML for this demo is comprised of:
- Icon Containers - a container div with all the information about individual icons.
- A cover - a cover div which is on top of the entire demo, which will blur if the user long presses to show the sub menu.
- A few notifications - a few notifications at the bottom to give some instructions on what to do.
- One sub menu - one sub menu which is moved around as the user clicks on various icons.
Step 2: CSS
Most of the CSS is quite basic, however I will call out a few important and interesting things I have used which are pretty typical of HTML UI design. So our goal is to make the icons draggable, but there's a lot in the way that can affect that. For one, we have a cover that pops up to blur out the background. Then we have those pesky images, which most browsers let users drag.
To get around most of these issues, we can use a property and value in CSS called pointer-events: none;
. This means the user can't interact with our HTML elements. So on the blurred cover, we remove pointer events when it's not blurred, and add them on when the background blurs out. This means the user normally clicks through this HTML element, until we want to blur the background, at which point it becomes active:
#iphone .cover {
position: absolute;
top: -1rem;
backdrop-filter: blur(4px);
left: -1rem;
width: calc(100% + 2rem);
opacity: 0;
z-index: 99999;
height: calc(100% + 2rem);
transition: all 0.2s ease-out;
background: rgba(0,0,0,0);
pointer-events: none; /* No pointer events! */
}
[data-dropdown="true"] #iphone .cover {
pointer-events: all; /* All pointer events */
opacity: 1;
}
#iphone .icon-container img {
width: calc(100% + 2px);
pointer-events: none; /* No pointer events */
}
Backdrop Filter
This brings us onto another CSS property which can be quite useful - backdrop-filter
. With support in every major browser except Firefox, backdrop filters allow us to add effects to the elements behind an HMTL tag if the tag is slightly transparent. In our .cover
CSS we have it defined as so:
backdrop-filter: blur(4px);
Animating Icon Removal
For this whole demo, I wanted icon removal to be smooth. The icons are arranged according to a flex box - so to do this I've created a custom animation which causes the icon to scale to effectively zero (so that it appears they zoom away) - and then reduces their width in tandem. The effect, is that the icons seem to close in on each other as one is removed, creating a smooth animation.
This animation is applied in a small piece of Javascript when the user clicks on a removal button.
@keyframes scaleBack {
0% {
transform: scale(1);
width: 5rem;
}
40% {
opacity: 1;
}
50% {
width: 5rem;
opacity: 0;
transform: scale(0.0001);
}
100% {
width: 0rem;
transform: scale(0.0001);
}
}
Positives to using transforms
- Let's us use top, bottom, left and right positioning for other things.
- Uses 3d acceleration, so animates super fast!
Step 2: Javascript
The Javascript here is not super complicated - and in fact a lot of the complexity comes from trying to make the icons draggable. There are essentially a couple of events we want to track:
- When the user clicks on an icon - we want to measure for how long, so we can show the dropdown or the shaking effect.
- When the user clicks off the button - we want to remove timers for long presses, and reset any other movement trackers.
- When the user clicks the removal button - we want to remove that icon.
- When the user clicks anything except a button - we want to remove the effects.
So, as you might have guessed, we use pointer events. I always recommend that you should try and use pointer events if you can, since they are both mobile and desktop compatible. That way, you usually only need to write one script for all devices. Pointer Events replace event handlers like mousedown with pointerdown.
Tracking State
To track the user, we have two states - shaking, and dropdown. We use data attributes in the HTML tag to track this - if the icons are shaking then data-shaking="true"
on the body tag, and if a dropdown is visible, then data-dropdown="true"
is shown on the body tag. We do it this way so that we can change the CSS of the elements based on the state.
We then have the long press. To manage long presses, all we have to do is:
- Create a pointerdown variable - this variable is set to true immediately when the user clicks, and it is set to false immediately when they release.
- Create timeouts - we check after 1 second and 2 seconds to see if pointerdown is true. If it is, then we either cause the icons to show the dropdown, or shake.
- Set a reset function - We reset all variables should the user give up on the long press, so we can use them again if they try once more.
In code, this looks like this:
// For selection of the icons and removal icons
let icons = document.querySelectorAll('.icon-container');
let removals = document.querySelectorAll('.remove-icon');
// These all store data on the mouse position at different times in the code
let pointerdown = false;
let offset = 0;
let mouseXInit = 0;
let mouseYInit = 0;
let mouseX = 0;
let mouseY = 0;
let positionX = 0;
let positionY = 0;
let currentTop = 0;
let currentLeft = 0;
// This is for holding our timers
let timers = { first: undefined, second: undefined, third: undefined }
let helpers = {
reset: function(extended) {
// This is our reset - it sets everything back to zero, whenever we need to
// All variables and settings are reset
mouseX = 0;
mouseY = 0;
mouseXInit = 0;
mouseYInit = 0;
currentTop = 0;
currentLeft = 0;
offset = 0;
if(typeof timers.first !== "undefined") {
clearTimeout(timers.first);
}
if(typeof timers.second !== "undefined") {
clearTimeout(timers.second);
}
if(typeof timers.third !== "undefined") {
clearTimeout(timers.third);
}
if(typeof extended == "undefined") {
document.querySelector('.sub-menu').classList.remove('show-sub-menu');
document.body.setAttribute('data-shaking', false);
document.body.setAttribute('data-dropdown', false);
pointerdown = false;
icons.forEach(function(item) {
item.setAttribute('data-selected', false);
item.style.top = 0;
item.style.left = 0;
})
}
},
checkPoint: function(x, y, limit) {
// This checks if the users mouse has moved more than a certain limit. If it has, then they may be dragging..
// So we don't cause the long press animation
if(x < limit && x > limit * -1 && y < limit && y > limit * -1) {
return true;
} else {
return false;
}
}
}
// For every icon
icons.forEach(function(item) {
// Add a pointerdown event
item.addEventListener('pointerdown', function(e) {
// Get the click location and set pointerdown to true
pointerdown = true;
mouseXInit = e.pageX;
mouseYInit = e.pageY;
// Get the left and top position of the item, if any
currentTop = parseFloat(item.style.top) || 0;
currentLeft = parseFloat(item.style.left) || 0;
// Set a timer to wait for a hold click
timers.first = setTimeout(function() {
// Only do this if pointerdown is true, and if the user hasn't moved more than 10px while clicking down
if(pointerdown === true && document.body.getAttribute('data-shaking') !== "true" && helpers.checkPoint(mouseX, mouseY, 10)) {
// Icon is now selected, and the dropdown should appear
item.setAttribute('data-selected', true);
document.body.setAttribute('data-dropdown', true);
// Find out where exactly the icon is (x, y) coordinates
let left = item.getBoundingClientRect().left - document.querySelector('#iphone').getBoundingClientRect().left;
let bottom = item.getBoundingClientRect().bottom - document.querySelector('#iphone').getBoundingClientRect().top;
// Show the sub menu and move it to where the icon is
document.querySelector('.sub-menu').classList.add('show-sub-menu');
document.querySelector('.sub-menu').style.left = `${left}px`;
document.querySelector('.sub-menu').style.top = `${bottom - 16}px`;
}
}, 1000);
// If the user is still clicking after 2 seconds
timers.second = setTimeout(function() {
// Check they are clicking
if(pointerdown === true && helpers.checkPoint(mouseX, mouseY, 10)) {
// Now all icons should shake
document.body.setAttribute('data-shaking', true);
item.setAttribute('data-dragging', true);
// Hide the sub menu
document.querySelector('.sub-menu').classList.remove('show-sub-menu');
document.body.setAttribute('data-dropdown', false);
// Give each animation for shaking a delay, to give the appearance of randomness
timers.third = setTimeout(function() {
icons.forEach(function(i) {
i.style.animationDelay = `${offset}s`;
offset += 0.1;
})
}, 300);
}
}, 2000);
// If the icons are shaking, then the user may be trying to drag this particular icon. Set that icon
// to have a data-dragging of true. We can use this later
if(document.body.getAttribute('data-shaking') === "true") {
item.setAttribute('data-dragging', true);
}
});
// if the user lifts their mouse, then reset everything
item.addEventListener('pointerup', function() {
helpers.reset(false);
});
})
Removal
When the user clicks an icon, then we have to animate that icon's removal. To do that, we first add an animation which causes the icon to zoom away, and reduce to a width of 0
. We spoke about this in the previous, CSS section. Then after the animation is over, we set another timeout to remove the icon entirely from the HTML.
removals.forEach(function(item) {
item.addEventListener('click', function(e) {
// If the removal icon is clicked, then get the parent HTML element - i.e. the icon itself
let icon = item.parentNode;
// Animate the icon to disappear
icon.style.animation = 'scaleBack 0.4s linear 1 forwards';
// Remove the dropdown, if it is around
document.body.setAttribute('data-dropdown', false);
// And finally, delete the icon completely using the remove() function.
setTimeout(() => {
icon.remove();
}, 400);
})
});
Draggability
Next, let's implement a basic form of dragging. Dragging conceptually can be broken down into a few pieces:
- Firstly, when the user clicks on the page, we find out exactly where they clicked, using
e.clientX, e.clientY
. This gives us the coordinates of their click. - As they move with their pointer clicked down, we then find the difference between their new position, and that original click position. That difference is the total amount moved.
- We then add that amount to the
top
andleft
CSS values for that icon. This gives us the drag effect. Eventually, if the user stops dragging, we reset all values so the icon snaps back to its original place.
Since we need to track quite a few things, we have a lot of variables. We can track the initial mouse position with mouseXInit, mouseYInit
. Then, the difference is stored in positionX, positionY
, after shaking is activated. We also store a separate mouseX, mouseY
before shaking starts. If the user moves too much while clicking down, we don't activate the long press effect, so we can check that with mouseX, mouseY
.
In code, we end up with this:
document.body.addEventListener('mousemove', function(e) {
// If the user is clicking down
if(pointerdown === true) {
// Track how much they're moving. If it's too much, we'll cancel the long press timeout
mouseY = mouseXInit - e.pageY;
mouseX = mouseXInit - e.pageX;
if(document.body.getAttribute('data-shaking') == "true") {
// If they are moving around after shaking starts, then they are dragging
positionX = mouseXInit - e.pageX;
positionY = mouseYInit - e.pageY;
// Set the element to have a data-dragging attribute of true
let el = document.querySelector('[data-dragging="true"]');
if(el !== null) {
// Move the element around
el.style.top = `${positionY * -1 + currentTop}px`;
el.style.left = `${positionX * -1 + currentLeft}px`;
}
}
}
})
// When the user lifts their pointer up, then reset all the variables
document.body.addEventListener('pointerup', function(e) {
if(!e.target.matches('.remove-icon')) {
helpers.reset(false);
}
// And end all icon dragging by setting data-dragging to false on all icons.
icons.forEach(function(item) {
item.setAttribute('data-dragging', false);
});
});
Conclusion
That about wraps it up for this tutorial. I hope you've enjoyed this, and maybe picked up a few new CSS skills. As always, here are some useful links:
More Tips and Tricks for Javascript
- Javascript Temporal and How it Works
- Waiting for the DOM to be ready in Javascript
- Javascript ShadowRealms
- A Guide to Heaps, Stacks, References and Values in Javascript
- The Free Course for Javascript
- Deleting an Item in an Array at a Specific Index
- Javascript: Check if an Array is a Subset of Another Array
- Making your own Express Middleware
- Making a Morphing 3D Sphere in Javascript with Three.js
- Websockets Tutorial: Creating a real-time Websocket Server