How nice or fun can we make the interactions on a website or web application? The truth is that most could be better than we do today. For example, who would not want to use an application like this:
Credit: Jakub Antalik on dribble
In this tutorial we will see how to implement a creative component to upload files, using as inspiration the previous animation by Jakub Antalík. The idea is to bring better visual feedback around what happens with the file after is dropped.
We will be focusing only on implementing the drag
and drop
interactions and some animations, without actually implementing all the necessary logic to actually upload the files to the server and use the component in production.
This is what our component will look like:
You can see the live demo or play with the code in Codepen. But if you also want to know how it works, just keep reading.
During the tutorial we will be seeing two main aspects:
drag
and drop
events.In addition to the usual technologies (HTML, CSS, Javascript), to code our component we will use the lightweight animation library anime.js.
In this case our HTML structure will be quite basic:
<!-- Form to upload the files -->
<form class="upload" method="post" action="" enctype="multipart/form-data" novalidate="">
<!-- The `input` of type `file` -->
<input class="upload__input" name="files[]" type="file" multiple=""/>
<!-- The `canvas` element to draw the particles -->
<canvas class="upload__canvas"></canvas>
<!-- The upload icon -->
<div class="upload__icon"><svg viewBox="0 0 470 470"><path d="m158.7 177.15 62.8-62.8v273.9c0 7.5 6 13.5 13.5 13.5s13.5-6 13.5-13.5v-273.9l62.8 62.8c2.6 2.6 6.1 4 9.5 4 3.5 0 6.9-1.3 9.5-4 5.3-5.3 5.3-13.8 0-19.1l-85.8-85.8c-2.5-2.5-6-4-9.5-4-3.6 0-7 1.4-9.5 4l-85.8 85.8c-5.3 5.3-5.3 13.8 0 19.1 5.2 5.2 13.8 5.2 19 0z"></path></svg></div>
</form>
As you can see, we only need a form
element and a file
type input
to allow the upload of files to the server. In our component we also need a canvas
element to draw the particles and an SVG icon.
Keep in mind that to use a component like this in production, you must fill in the action
attribute in the form, and perhaps add a label
element for the input, etc.
We will be using SCSS as the CSS preprocessor, but the styles we are using are very close to being plain CSS and they are quite simple.
Let’s start by positioning the form
and canvas
elements, among other basic styles:
// Position `form` and `canvas` full width and height
.upload, .upload__canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
// Position the `canvas` behind all other elements
.upload__canvas {
z-index: -1;
}
// Hide the file `input`
.upload__input {
display: none;
}
Now let’s see the styles needed for our form
, both for the initial state (hidden) and for when it is active (the user is dragging files to upload). The code has been commented exhaustively for a better understanding:
// Styles for the upload `form`
.upload {
z-index: 1; // should be the higher `z-index`
// Styles for the `background`
background-color: rgba(4, 72, 59, 0.8);
background-image: radial-gradient(ellipse at 50% 120%, rgba(4, 72, 59, 1) 10%, rgba(4, 72, 59, 0) 40%);
background-position: 0 300px;
background-repeat: no-repeat;
// Hide it by default
opacity: 0;
visibility: hidden;
// Transition
transition: 0.5s;
// Upload overlay, that prevent the event `drag-leave` to be triggered while dragging over inner elements
&:after {
position: absolute;
content: '';
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
// Styles applied while files are being dragging over the screen
.upload--active {
// Translate the `radial-gradient`
background-position: 0 0;
// Show the upload component
opacity: 1;
visibility: visible;
// Only transition `opacity`, preventing issues with `visibility`
transition-property: opacity;
}
Finally, let’s look at the simple styles that we have applied to the upload icon:
// Styles for the icon
.upload__icon {
position: relative;
left: calc(50% - 40px);
top: calc(50% - 40px);
width: 80px;
height: 80px;
padding: 15px;
border-radius: 100%;
background-color: #EBF2EA;
path {
fill: rgba(4, 72, 59, 0.8);
}
}
Now our component looks like we want, so we’re ready to add interactivity with Javascript.
Before implementing the drag
and drop
functionality, let’s see how we can implement a particle system.
In our particle system, each particle will be a simple Javascript Object
with basic parameters to define how the particle should behave. And all the particles will be stored in an Array
, which in our code is called particles
.
Then, adding a new particle to our system is a matter of creating a new Javascrit Object
and adding it to the particles
array. Check the comments so you understand the purpose of each property:
// Create a new particle
function createParticle(options) {
var o = options || {};
particles.push({
'x': o.x, // particle position in the `x` axis
'y': o.y, // particle position in the `y` axis
'vx': o.vx, // in every update (animation frame) the particle will be translated this amount of pixels in `x` axis
'vy': o.vy, // in every update (animation frame) the particle will be translated this amount of pixels in `y` axis
'life': 0, // in every update (animation frame) the life will increase
'death': o.death || Math.random() * 200, // consider the particle dead when the `life` reach this value
'size': o.size || Math.floor((Math.random() * 2) + 1) // size of the particle
});
}
Now that we have defined the basic structure of our particle system, we need a loop function, which allows us to add new particles, update them and draw them on the canvas
in each animation frame. Something like this:
// Loop to redraw the particles on every frame
function loop() {
addIconParticles(); // add new particles for the upload icon
updateParticles(); // update all particles
renderParticles(); // clear `canvas` and draw all particles
iconAnimationFrame = requestAnimationFrame(loop); // loop
}
Now let’s see how we have defined all the functions that we call inside the loop. As always, pay attention to the comments:
// Add new particles for the upload icon
function addIconParticles() {
iconRect = uploadIcon.getBoundingClientRect(); // get icon dimensions
var i = iconParticlesCount; // how many particles we should add?
while (i--) {
// Add a new particle
createParticle({
x: iconRect.left + iconRect.width / 2 + rand(iconRect.width - 10), // position the particle along the icon width in the `x` axis
y: iconRect.top + iconRect.height / 2, // position the particle centered in the `y` axis
vx: 0, // the particle will not be moved in the `x` axis
vy: Math.random() * 2 * iconParticlesCount // value to move the particle in the `y` axis, greater is faster
});
}
}
// Update the particles, removing the dead ones
function updateParticles() {
for (var i = 0; i < particles.length; i++) {
if (particles[i].life > particles[i].death) {
particles.splice(i, 1);
} else {
particles[i].x += particles[i].vx;
particles[i].y += particles[i].vy;
particles[i].life++;
}
}
}
// Clear the `canvas` and redraw every particle (rect)
function renderParticles() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
for (var i = 0; i < particles.length; i++) {
ctx.fillStyle = 'rgba(255, 255, 255, ' + (1 - particles[i].life / particles[i].death) + ')';
ctx.fillRect(particles[i].x, particles[i].y, particles[i].size, particles[i].size);
}
}
And we have our particle system ready, where we can add new particles defining the options we want, and the loop will be responsible for performing the animation.
Now let’s see how we prepare the upload icon to be animated:
// Add 100 particles for the icon (without render), so the animation will not look empty at first
function initIconParticles() {
var iconParticlesInitialLoop = 100;
while (iconParticlesInitialLoop--) {
addIconParticles();
updateParticles();
}
}
initIconParticles();
// Alternating animation for the icon to translate in the `y` axis
function initIconAnimation() {
iconAnimation = anime({
targets: uploadIcon,
translateY: -10,
duration: 800,
easing: 'easeInOutQuad',
direction: 'alternate',
loop: true,
autoplay: false // don't execute the animation yet, only on `drag` events (see later)
});
}
initIconAnimation();
With the previous code, we only need a couple of other functions to pause or resume the animation of the upload icon, as appropriate:
// Play the icon animation (`translateY` and particles)
function playIconAnimation() {
if (!playingIconAnimation) {
playingIconAnimation = true;
iconAnimation.play();
iconAnimationFrame = requestAnimationFrame(loop);
}
}
// Pause the icon animation (`translateY` and particles)
function pauseIconAnimation() {
if (playingIconAnimation) {
playingIconAnimation = false;
iconAnimation.pause();
cancelAnimationFrame(iconAnimationFrame);
}
}
Then we can start adding the drag
and drop
functionality to upload the files. Let’s start by preventing unwanted behaviors for each related event:
// Preventing the unwanted behaviours
['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach(function (event) {
document.addEventListener(event, function (e) {
e.preventDefault();
e.stopPropagation();
});
});
Now we will handle the events of type drag
, where we will activate the form
so that it is shown, and we will play the animations for the upload icon:
// Show the upload component on `dragover` and `dragenter` events
['dragover', 'dragenter'].forEach(function (event) {
document.addEventListener(event, function () {
if (!animatingUpload) {
uploadForm.classList.add('upload--active');
playIconAnimation();
}
});
});
In case the user leaves the drop
zone, we simply hide the form
again and pause the animations for the upload icon:
// Hide the upload component on `dragleave` and `dragend` events
['dragleave', 'dragend'].forEach(function (event) {
document.addEventListener(event, function () {
if (!animatingUpload) {
uploadForm.classList.remove('upload--active');
pauseIconAnimation();
}
});
});
And finally the most important event that we must handle is the drop
event, because it will be where we will obtain the files that the user has dropped, we will execute the corresponding animations, and if this were a fully functional component we would upload the files to the server through AJAX.
// Handle the `drop` event
document.addEventListener('drop', function (e) {
if (!animatingUpload) { // If no animation in progress
droppedFiles = e.dataTransfer.files; // the files that were dropped
filesCount = droppedFiles.length > 3 ? 3 : droppedFiles.length; // the number of files (1-3) to perform the animations
if (filesCount) {
animatingUpload = true;
// Add particles for every file loaded (max 3), also staggered (increasing delay)
var i = filesCount;
while (i--) {
addParticlesOnDrop(e.pageX + (i ? rand(100) : 0), e.pageY + (i ? rand(100) : 0), 200 * i);
}
// Hide the upload component after the animation
setTimeout(function () {
uploadForm.classList.remove('upload--active');
}, 1500 + filesCount * 150);
// Here is the right place to call something like:
// triggerFormSubmit();
// A function to actually upload the files to the server
} else { // If no files where dropped, just hide the upload component
uploadForm.classList.remove('upload--active');
pauseIconAnimation();
}
}
});
In the previous code snippet we saw that the function addParticlesOnDrop
is called, which is in charge of executing the particle animation from where the files were dropped. Let’s see how we can implement this function:
// Create a new particles on `drop` event
function addParticlesOnDrop(x, y, delay) {
// Add a few particles when the `drop` event is triggered
var i = delay ? 0 : 20; // Only add extra particles for the first item dropped (no `delay`)
while (i--) {
createParticle({
x: x + rand(30),
y: y + rand(30),
vx: rand(2),
vy: rand(2),
death: 60
});
}
// Now add particles along the way where the user `drop` the files to the icon position
// Learn more about this kind of animation in the `anime.js` documentation
anime({
targets: {x: x, y: y},
x: iconRect.left + iconRect.width / 2,
y: iconRect.top + iconRect.height / 2,
duration: 500,
delay: delay || 0,
easing: 'easeInQuad',
run: function (anim) {
var target = anim.animatables[0].target;
var i = 10;
while (i--) {
createParticle({
x: target.x + rand(30),
y: target.y + rand(30),
vx: rand(2),
vy: rand(2),
death: 60
});
}
},
complete: uploadIconAnimation // call the second part of the animation
});
}
Finally, when the particles reach the position of the icon, we must move the icon upwards, giving the impression that the files are being uploaded:
// Translate and scale the upload icon
function uploadIconAnimation() {
iconParticlesCount += 2; // add more particles per frame, to get a speed up feeling
anime.remove(uploadIcon); // stop current animations
// Animate the icon using `translateY` and `scale`
iconAnimation = anime({
targets: uploadIcon,
translateY: {
value: -canvasHeight / 2 - iconRect.height,
duration: 1000,
easing: 'easeInBack'
},
scale: {
value: '+=0.1',
duration: 2000,
elasticity: 800
},
complete: function () {
// reset the icon and all animation variables to its initial state
setTimeout(resetAll, 0);
}
});
}
To finish, we must implement the resetAll
function, which resets the icon and all the variables to its initial state. We must also update the canvas
size and reset the component on resize
event. But in order not to make this tutorial any longer, we have not included these and other minor details, although you can check the complete code in the Github repository.
And finally our component is complete! Let’s take a look:
You can check the live demo, play with the code on Codepen, or get the full code on Github.
Throughout the tutorial we saw how to create a simple particle system, as well as handle drag
and drop
events to implement an eye-catching file upload component.
Remember that this component is not ready to be used in production. In case you want to complete the implementation to make it fully functional, I recommend checking this excellent tutorial in CSS Tricks.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!