Build a Javascript Animated Custom File Upload Modal with Progress Bar

By Bytewebster - December 29, 2022



Welcome to the bytewebster Project blogs. In this project, we have created a custom animated upload modal with the help of HTML, CSS, and JavaScript.

Working:

This file upload modal comes with an amazing user interface. In this, whenever you upload any file, image, video, or document. at the same time, you will see a progress bar, and with the help of this progress bar, you will know how much the file has been uploaded. A success message will appear after the file is uploaded successfully, And if for some reason the file is not uploaded, then in the same model you will be shown a message of a failed.

                     
                 
Detailed Overview of Project

After the file is uploaded in this file upload model, you can also copy the link of that uploaded file. Only HTML, CSS, and JavaScript have been used to make this project. This file upload model is fully responsive, its design will not deteriorate on any device, and it can be used on every device.


HTML Structure


<div id="upload" class="modal" data-state="0" data-ready="false">
	<div class="modal__header">
		<button class="modal__close-button" type="button">
			<svg class="modal__close-icon" viewBox="0 0 16 16" width="16px" height="16px" aria-hidden="true">
				<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
					<polyline points="1,1 15,15" />
					<polyline points="15,1 1,15" />
				</g>
			</svg>
			<span class="modal__sr">Close</span>
		</button>
	</div>
	<div class="modal__body">
		<div class="modal__col">
			<!-- up -->
			<svg class="modal__icon modal__icon--blue" viewBox="0 0 24 24" width="24px" height="24px" aria-hidden="true">
				<g fill="none" stroke="hsl(223,90%,50%)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
					<circle class="modal__icon-sdo69" cx="12" cy="12" r="11" stroke-dasharray="69.12 69.12" />
					<polyline class="modal__icon-sdo14" points="7 12 12 7 17 12" stroke-dasharray="14.2 14.2" />
					<line class="modal__icon-sdo10" x1="12" y1="7" x2="12" y2="17" stroke-dasharray="10 10" />
				</g>
			</svg>
			<!-- error -->
			<svg class="modal__icon modal__icon--red" viewBox="0 0 24 24" width="24px" height="24px" aria-hidden="true" display="none">
				<g fill="none" stroke="hsl(3,90%,50%)" stroke-width="2" stroke-linecap="round">
					<circle class="modal__icon-sdo69" cx="12" cy="12" r="11" stroke-dasharray="69.12 69.12" />
					<line class="modal__icon-sdo14" x1="7" y1="7" x2="17" y2="17" stroke-dasharray="14.2 14.2" />
					<line class="modal__icon-sdo14" x1="17" y1="7" x2="7" y2="17" stroke-dasharray="14.2 14.2" />
				</g>
			</svg>
			<!-- check -->
			<svg class="modal__icon modal__icon--green" viewBox="0 0 24 24" width="24px" height="24px" aria-hidden="true" display="none">
				<g fill="none" stroke="hsl(138,90%,50%)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
					<circle class="modal__icon-sdo69" cx="12" cy="12" r="11" stroke-dasharray="69.12 69.12" />
					<polyline class="modal__icon-sdo14" points="7 12.5 10 15.5 17 8.5" stroke-dasharray="14.2 14.2" />
				</g>
			</svg>
		</div>
		<div class="modal__col">
			<div class="modal__content">
				<h2 class="modal__title">Upload a File</h2>
				<p class="modal__message">Select a file to upload from your computer or device.</p>
				<div class="modal__actions">
					<button class="modal__button modal__button--upload" type="button" data-action="file">Choose File</button>
					<input id="file" type="file" hidden>
				</div>
				<div class="modal__actions" hidden>
					<svg class="modal__file-icon" viewBox="0 0 24 24" width="24px" height="24px" aria-hidden="true">
						<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
							<polygon points="4 1 12 1 20 8 20 23 4 23" />
							<polyline points="12 1 12 8 20 8" />
						</g>
					</svg>
					<div class="modal__file" data-file></div>
					<button class="modal__close-button" type="button" data-action="fileReset">
						<svg class="modal__close-icon" viewBox="0 0 16 16" width="16px" height="16px" aria-hidden="true">
							<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
								<polyline points="4,4 12,12" />
								<polyline points="12,4 4,12" />
							</g>
						</svg>
						<span class="modal__sr">Remove</span>
					</button>
					<button class="modal__button" type="button" data-action="upload">Upload</button>
				</div>
			</div>
			<div class="modal__content" hidden>
				<h2 class="modal__title">Uploading…</h2>
				<p class="modal__message">Just give us a moment to process your file.</p>
				<div class="modal__actions">
					<div class="modal__progress">
						<div class="modal__progress-value" data-progress-value>0%</div>
						<div class="modal__progress-bar">
							<div class="modal__progress-fill" data-progress-fill></div>
						</div>
					</div>
					<button class="modal__button" type="button" data-action="cancel">Cancel</button>
				</div>
			</div>
			<div class="modal__content" hidden>
				<h2 class="modal__title">Oops!</h2>
				<p class="modal__message">Your file could not be uploaded due to an error. Try uploading it again?</p>
				<div class="modal__actions modal__actions--center">
					<button class="modal__button" type="button" data-action="upload">Retry</button>
					<button class="modal__button" type="button" data-action="cancel">Cancel</button>
				</div>
			</div>
			<div class="modal__content" hidden>
				<h2 class="modal__title">Upload Successful!</h2>
				<p class="modal__message">Your file has been uploaded. You can copy the link to your clipboard.</p>
				<div class="modal__actions modal__actions--center">
					<button class="modal__button" type="button" data-action="copy">Copy Link</button>
					<button class="modal__button" type="button" data-action="cancel">Done</button>
				</div>
			</div>
		</div>
	</div>
</div>

let's start with its HTML structure. First of all, in this we have created a class named Modal. Inside this, we have created two more classes, the first class for the header of the model and the second class for the body of the model.

There is an SVG icon in the header of this file upload modal, with the help of this modal will be closed.

This file upload model uses SVG (Scalable Vector Graphics) for icons Inside the body class, there is another div tag in which all the icons are placed.

After the code starts with the container for all of the content in this modal The next line is an h2 tag that has been given the class "modal__title". The text inside of it will be used as the title for this modal. Next, there's a paragraph tag with some text inside of it that will be displayed when you click on "Choose File" button.

Next, we have an input field for uploading files. It has a type attribute set to file and data-action="upload". The next line of code starts with a button that says "Cancel". When you click on this button, it will cancel out of the modal. The code is a modal that displays an error message to the user.



Styling With CSS


.modal {
	background-color: hsl(var(--hue),10%,95%);
	border-radius: 1em;
	box-shadow: 0 0.75em 1em hsla(var(--hue),10%,5%,0.3);
	color: hsl(var(--hue),10%,5%);
	width: calc(100% - 3em);
	max-width: 34.5em;
	overflow: hidden;
	position: relative;
	transition:
		background-color var(--trans-dur),
		color var(--trans-dur);
}
.modal:before {
	background-color: hsl(223,90%,60%);
	border-radius: 50%;
	content: "";
	filter: blur(60px);
	opacity: 0.15;
	position: absolute;
	top: -8em;
	right: -15em;
	width: 25em;
	height: 25em;
	z-index: 0;
	transition: background-color var(--trans-dur);
}
.modal__actions {
	animation-delay: 0.2s;
	display: flex;
	align-items: center;
	flex-wrap: wrap;
}
.modal__body,.modal__header {
	position: relative;
	z-index: 1;
}
.modal__body {
	display: flex;
	flex-direction: column;
	padding: 0 2em 1.875em 1.875em;
}
.modal__button,.modal__close-button {
	color: currentColor;
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
}
.modal__button {
	background-color: hsla(var(--hue),10%,50%,0.2);
	border-radius: 0.25rem;
	font-size: 0.75em;
	padding: 0.5rem 2rem;
	transition:
		background-color var(--trans-dur),
		border-color var(--trans-dur),
		opacity var(--trans-dur);
	width: 100%;
}
.modal__button + .modal__button {
	margin-top: 0.75em;
}
.modal__button:disabled {
	opacity: 0.5;
}
.modal__button:focus,.modal__close-button:focus {
	outline: transparent;
}
.modal__button:hover,.modal__button:focus-visible {
	background-color: hsla(var(--hue),10%,60%,0.2);
}
.modal__button--upload {
	background-color: transparent;
	border: 0.125rem dashed hsla(var(--hue),10%,50%,0.4);
	flex: 1;
	padding: 0.375rem 2rem;
}
.modal__col + .modal__col {
	flex: 1;
	margin-top: 1.875em;
}
.modal__close-button,.modal__message,.modal__progress-value {
	color: hsl(var(--hue),10%,30%);
	transition: color var(--trans-dur);
}
.modal__close-button {
	background-color: transparent;
	display: flex;
	width: 1.5em;
	height: 1.5em;
	transition: color var(--trans-dur);
}
.modal__close-button:hover,.modal__close-button:focus-visible {
	color: hsl(var(--hue),10%,40%);
}
.modal__close-icon {
	display: block;
	margin: auto;
	pointer-events: none;
	width: 50%;
	height: auto;
}
.modal__content > * {
	animation-name: fadeSlideIn;
	animation-duration: 0.5s;
	animation-timing-function: ease-in-out;
	animation-fill-mode: forwards;
	opacity: 0;
}
.modal__file {
	flex: 1;
	font-size: 0.75em;
	font-weight: 700;
	margin-right: 0.25rem;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.modal__file ~ .modal__button {
	margin-top: 1.5em;
}
.modal__file-icon {
	color: hsl(var(--hue),10%,50%);
	display: block;
	margin-right: 0.75em;
	width: 1.5em;
	height: 1.5em;
	transition: color var(--trans-dur);
}
.modal__header {
	display: flex;
	justify-content: flex-end;
	align-items: center;
	height: 2.5em;
	padding: 0.5em;
}
.modal__icon {
	display: block;
	margin: auto;
	width: 2.25em;
	height: 2.25em;
}
.modal__icon--blue g {
	stroke: hsl(223,90%,50%);
}
.modal__icon--red g {
	stroke: hsl(3,90%,50%);
}
.modal__icon--green g {
	stroke: hsl(138,90%,40%);
}
.modal__icon circle,.modal__icon line,.modal__icon polyline {
	animation: sdo 0.25s ease-in-out forwards;
	transition: stroke var(--trans-dur);
}
.modal__icon :nth-child(2) {
	animation-delay: 0.25s;
}
.modal__icon :nth-child(3) {
	animation-delay: 0.5s;
}
.modal__icon-sdo10 {
	stroke-dashoffset: 10;
}
.modal__icon-sdo14 {
	stroke-dashoffset: 14.2;
}
.modal__icon-sdo69 {
	stroke-dashoffset: 69.12;
	transform: rotate(-90deg);
	transform-origin: 12px 12px;
}
.modal__message {
	animation-delay: 0.1s;
	font-size: 1em;
	margin-bottom: 1.5em;
	min-height: 3em;
}
.modal__progress {
	flex: 1;
}
.modal__progress + .modal__button {
	margin-top: 1.75em;
}
.modal__progress-bar {
	background-image: linear-gradient(90deg,hsl(var(--hue),90%,50%),hsl(var(--hue),90%,70%));
	border-radius: 0.2em;
	overflow: hidden;
	width: 100%;
	height: 0.4em;
	transform: translate3d(0,0,0);
}
.modal__progress-fill {
	background-color: hsl(var(--hue),10%,90%);
	width: inherit;
	height: inherit;
	transition: transform 0.1s ease-in-out;
}
.modal__progress-value {
	font-size: 0.75em;
	font-weight: 700;
	line-height: 1.333;
	text-align: right;
}
.modal__sr {
	overflow: hidden;
	position: absolute;
	width: 1px;
	height: 1px;
}
.modal__title {
	font-size: 1.25em;
	font-weight: 500;
	line-height: 1.2;
	margin-bottom: 1.5rem;
	text-align: center;
}
/* state change */
[data-state="2"]:before {
	background-color: hsl(3,90%,60%);
}
[data-state="3"]:before {
	background-color: hsl(138,90%,60%);
}
.modal__icon + .modal__icon,
[data-state="1"] .modal__icon:first-child,
[data-state="2"] .modal__icon:first-child,
[data-state="3"] .modal__icon:first-child,
.modal__content + .modal__content,
[data-state="1"] .modal__content:first-child,
[data-state="2"] .modal__content:first-child,
[data-state="3"] .modal__content:first-child {
	display: none;
}
[data-state="1"] .modal__icon:first-child,
[data-state="2"] .modal__icon:nth-child(2),
[data-state="3"] .modal__icon:nth-child(3),
[data-state="1"] .modal__content:nth-child(2),
[data-state="2"] .modal__content:nth-child(3),
[data-state="3"] .modal__content:nth-child(4) {
	display: block;
}
[data-ready="false"] .modal__content:first-child .modal__actions:nth-of-type(2),
[data-ready="true"] .modal__content:first-child .modal__actions:first-of-type {
	display: none;
}
[data-ready="true"] .modal__content:first-child .modal__actions:nth-of-type(2) {
	display: flex;
}
@media (prefers-color-scheme: dark) {
	:root {
		--bg: hsl(var(--hue),10%,35%);
		--fg: hsl(var(--hue),10%,95%);
	}
	.modal {
		background-color: hsl(var(--hue),10%,10%);
		color: hsl(var(--hue),10%,95%);
	}
	.modal__close-button,
	.modal__message,
	.modal__progress-value {
		color: hsl(var(--hue),10%,70%);
	}
	.modal__close-button:hover,
	.modal__close-button:focus-visible {
		color: hsl(var(--hue),10%,80%);
	}
	.modal__file-icon {
		color: hsl(var(--hue),10%,60%);
	}
	.modal__icon--blue g {
		stroke: hsl(223,90%,60%);
	}
	.modal__icon--red g {
		stroke: hsl(3,90%,60%);
	}
	.modal__icon--green g {
		stroke: hsl(138,90%,60%);
	}
	.modal__progress-fill {
		background-color: hsl(var(--hue),10%,20%);
	}
}
@keyframes fadeSlideIn {
	from { opacity: 0; transform: translateY(33%); }
	to { opacity: 1; transform: translateY(0); }
}
@keyframes sdo {
	to { stroke-dashoffset: 0; }
}

@media (min-width: 768px) {
	.modal__actions--center {
		justify-content: center;
		margin-left: -4.125em;
	}
	.modal__body {
		flex-direction: row;
		align-items: center;
	}
	.modal__button {
		width: auto;
	}
	.modal__button + .modal__button {
		margin-top: 0;
		margin-left: 1.5rem;
	}
	.modal__file ~ .modal__button {
		margin-top: 0;
	}
	.modal__file ~ .modal__close-button {
		margin-right: 1.5rem;
	}
	.modal__progress {
		margin-right: 2em;
	}
	.modal__progress + .modal__button {
		margin-top: 0;
	}
	.modal__col + .modal__col {
		margin-top: 0;
		margin-left: 1.875em;
	}
	.modal__title {
		text-align: left;
	}
}

So far only the structure of this file upload modal has been prepared, it is yet to be styled. We have used only basic CSS to style it. The demo code given above only contains the code for styling the modal. This code is given just for understanding, You can get the complete source code by clicking on the download button given at the bottom of this page.

The code of this custom file upload modal starts with a tag that has the class of modal, In this we have styled the interface of the modal. We used linear gradients to design the background of this file upload modal.

This model is designed and styled in a basic manner only. The design of the buttons or progress bar has also been kept simple, when you open the stylesheet of the project, So you will not face any problem understanding it. Now the last thing left is to bring it to work, which will be done with the help of JavaScript.


JavaScript Explanation


window.addEventListener("DOMContentLoaded",() => {
	const upload = new UploadModal("#upload");
});

class UploadModal {
    filename = "";
    isCopying = false;
    isUploading = false;
    progress = 0;
    progressTimeout = null;
    state = 0;

    constructor(el) {
        this.el = document.querySelector(el);
        this.el?.addEventListener("click",this.action.bind(this));
        this.el?.querySelector("#file")?.addEventListener("change",this.fileHandle.bind(this));
    }
    action(e) {
        this[e.target?.getAttribute("data-action")]?.();
        this.stateDisplay();
    }
    cancel() {
        this.isUploading = false;
        this.progress = 0;
        this.progressTimeout = null;
        this.state = 0;
        this.stateDisplay();
        this.progressDisplay();
        this.fileReset();
    }
    async copy() {
        const copyButton = this.el?.querySelector("[data-action='copy']");

        if (!this.isCopying && copyButton) {
            this.isCopying = true;
            copyButton.style.width = `${copyButton.offsetWidth}px`;
            copyButton.disabled = true;
            copyButton.textContent = "Copied!";
            navigator.clipboard.writeText(this.filename);
            await new Promise(res => setTimeout(res, 1000));
            this.isCopying = false;
            copyButton.removeAttribute("style");
            copyButton.disabled = false;
            copyButton.textContent = "Copy Link";
        }
    }
    fail() {
        this.isUploading = false;
        this.progress = 0;
        this.progressTimeout = null;
        this.state = 2;
        this.stateDisplay();
    }
    file() {
        this.el?.querySelector("#file").click();
    }
    fileDisplay(name = "") {
        this.filename = name;

        const fileValue = this.el?.querySelector("[data-file]");
        if (fileValue) fileValue.textContent = this.filename;

        this.el?.setAttribute("data-ready", this.filename ? "true" : "false");
    }
    fileHandle(e) {
        return new Promise(() => {
            const { target } = e;
            if (target?.files.length) {
                let reader = new FileReader();
                reader.onload = e2 => {
                    this.fileDisplay(target.files[0].name);
                };
                reader.readAsDataURL(target.files[0]);
            }
        });
    }
    fileReset() {
        const fileField = this.el?.querySelector("#file");
        if (fileField) fileField.value = null;

        this.fileDisplay();
    }
    progressDisplay() {
        const progressValue = this.el?.querySelector("[data-progress-value]");
        const progressFill = this.el?.querySelector("[data-progress-fill]");
        const progressTimes100 = Math.floor(this.progress * 100);

        if (progressValue) progressValue.textContent = `${progressTimes100}%`;
        if (progressFill) progressFill.style.transform = `translateX(${progressTimes100}%)`;
    }
    async progressLoop() {
        this.progressDisplay();

        if (this.isUploading) {
            if (this.progress === 0) {
                await new Promise(res => setTimeout(res, 1000));
                if (!this.isUploading) {
                    return;
                } else if (Utils.randomInt(0,2) === 0) {
                    this.fail();
                    return;
                }
            }
            if (this.progress < 1) {
                this.progress += 0.01;
                this.progressTimeout = setTimeout(this.progressLoop.bind(this), 50);
            } else if (this.progress >= 1) {
                this.progressTimeout = setTimeout(() => {
                    if (this.isUploading) {
                        this.success();
                        this.stateDisplay();
                        this.progressTimeout = null;
                    }
                }, 250);
            }
        }
    }
    stateDisplay() {
        this.el?.setAttribute("data-state", `${this.state}`);
    }
    success() {
        this.isUploading = false;
        this.state = 3;
        this.stateDisplay();
    }
    upload() {
        if (!this.isUploading) {
            this.isUploading = true;
            this.progress = 0;
            this.state = 1;
            this.progressLoop();
        }
    }
}

class Utils {
    static randomInt(min = 0,max = 2**32) {
        const percent = crypto.getRandomValues(new Uint32Array(1))[0] / 2**32;
        const relativeValue = (max - min) * percent;

        return Math.round(min + relativeValue);
    }
}

The JavaScript code of this animated file upload modal starts by creating a new UploadModal object.The constructor takes in an element as its argument and sets the el property to that element. It then adds an event listener for clicks on the el, which will trigger the action function of this class.

The next line is where things get interesting This line attaches a click handler to the document's click event so that when someone clicks on it, they are passed into our action function (which we'll define later).

This allows us to do anything we want with whatever was clicked on! For example, if you wanted to upload a file from your computer without having to open up another modal window, you could use this code.

After That the code starts by creating a new instance of the Uploader class.The constructor takes in an el and a function to call when the user clicks on it. It also creates an event listener for when the file changes, which is bound to this.fileHandle.

a new instance of the class called "FileUploader". The constructor for FileUploader is passed an object literal with three properties: filename, progress, and state. The first thing that happens in the constructor is that it creates a button element on the page.

This button will be used to start and stop uploading files. It also sets up some event listeners so when this button is clicked, it will call two functions: upload() and fail(). upload() starts by checking if there's already an uploaded file or not.

Whatever the main and important javascript code of this custom file upload model, we have defined it here. The rest of the code is very simple, you will not have any problem understanding it.


Thank you for spending your valuable time in reading this article. We hope you liked the project.




Video of the Project

Take This Short Survey!


Download Source Code Files

From here You can download the source code files of this animated file upload modal.
If you are just starting in web development, these snippets will be useful. We would appreciate it if you would share our blog posts with other like-minded people.


Download Source Code
Please wait ...
If the download didn't start automatically, click here

ByteWebster Play and Win Offer.

PLAY A SIMPLE GAME AND WIN PREMIUM WEB DESIGNS WORTH UPTO $100 FOR FREE.

PLAY FOR FREE





Connect With Us

we would like to keep in touch with you..... Register Here.

JOIN US JOIN TELEGRAM