Developing a Creative Upload Interaction with JavaScript and Canvas

Luis Manuel
๐Ÿ‘๏ธ 6,274 views
๐Ÿ’ฌ comments

How nice or fun can we do 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?

Dribble Shot by Jakub Antalรญk

I would upload things there, again and again, just to have the pleasure of watching the animation :)

Table of Contents

    Well 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:

    Creative Upload Interaction

    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:

    • We will learn how to implement a simple particle system using Javascript and Canvas.
    • We will implement everything necessary to handle drag and drop events to upload files.

    In addition to the usual technologies (HTML, CSS, Javascript), to code our component we will use the lightweight animation library anime.js.

    So without further ado, let's start!

    HTML structure

    In this case our HTML structure will be quite simple:

    <!-- 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>

    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, perhaps add a label element for the input, etc.

    Adding styles

    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.

    Developing a simple particle system

    Before implementing the drag and drop functionality, let's see how we can implement a simple 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 particles.

    Then, adding a new particle to our system is as simple as 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 || {};
            '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 simple 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
                x: iconRect.left + iconRect.width / 2 + rand(iconRect.width - 10), // position the particle along the icon width in the `x` axis
                y: + 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;
    // 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 simple particle system ready, where we can add new particles defining the options we want, and the loop will be responsible for performing the animation.

    Adding animations for the upload icon

    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--) {
    // 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)

    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;
            iconAnimationFrame = requestAnimationFrame(loop);
    // Pause the icon animation (`translateY` and particles)
    function pauseIconAnimation() {
        if (playingIconAnimation) {
            playingIconAnimation = false;

    Adding the drag and drop functionality

    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) {

    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) {

    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) {

    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 () {
                }, 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

    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--) {
                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
            targets: {x: x, y: y},
            x: iconRect.left + iconRect.width / 2,
            y: + iconRect.height / 2,
            duration: 500,
            delay: delay || 0,
            easing: 'easeInQuad',
            run: function (anim) {
                var target = anim.animatables[0].target;
                var i = 10;
                while (i--) {
                        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 the tutorial longer and heavier, 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:

    Creative Upload Interaction

    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.

    We hope you liked the final result, and that this tutorial has been useful!

    Luis Manuel

    11 posts

    Engineer in Computer Science and Freelance Front-End Developer. Available for Hire.