Let’s create an Animated File Upload Modal using HTML, CSS, and JavaScript. This project will allow users to upload files through a sleek and animated modal popup, adding style and interactivity to your web forms.
We’ll use:
- HTML to structure the modal and file upload input.
- CSS to design a smooth, responsive, and animated modal.
- JavaScript to open/close the modal and handle file input interactions.
This is a great mini-project for practicing modal logic, animations, and basic file input handling. Perfect for portfolios, dashboards, or any site needing a modern file upload feature. Let’s build it and give our UI a polished feel! 📁✨
HTML :
This HTML creates a file upload popup modal with:
- Close button
- Upload, progress, error, and success states
- File preview and progress bar
- Mobile-friendly and animated UI
It links to external CSS and JavaScript for full functionality.
<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>Animated Upload Modal | @coding.stella</title> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><link rel='stylesheet' href='https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;700&display=swap'><link rel="stylesheet" href="./style.css"> </head> <body> <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> <script src="./script.js"></script> </body> </html>
CSS :
This CSS styles a responsive modal popup with:
- Smooth animations and transitions
- Light and dark mode support
- Upload button and progress bar
- States like loading, error, success
- Neat layout for both mobile and desktop
It uses variables (--hue
) to control colors and has clean design using flexbox and media queries.
* { border: 0; box-sizing: border-box; margin: 0; padding: 0; } :root { --hue: 223; --bg: hsl(var(--hue),10%,85%); --fg: hsl(var(--hue),10%,5%); --trans-dur: 0.3s; font-size: calc(16px + (20 - 16) * (100vw - 320px) / (2560 - 320)); } body, button { font: 1em/1.5 Nunito, Helvetica, sans-serif; } body { background-color: var(--bg); color: var(--fg); height: 100vh; display: grid; place-items: center; transition: background-color var(--trans-dur), color var(--trans-dur); } .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 > * { /* don’t use shorthand syntax, or actions delay will be overridden */ 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; } /* Dark theme */ @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%); } } /* Animations */ @keyframes fadeSlideIn { from { opacity: 0; transform: translateY(33%); } to { opacity: 1; transform: translateY(0); } } @keyframes sdo { to { stroke-dashoffset: 0; } } /* Beyond mobile */ @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; } }
JavaScript:
This code sets up an upload modal that lets users select and upload a file, shows progress, handles success/failure, and allows copying the uploaded file’s name. When the page loads, it initializes the UploadModal
class on a DOM element. The class handles clicking actions (upload, cancel, copy), updates UI states, reads the selected file, shows progress through a loop, and simulates success or failure using a random number generator.
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) { // disable 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)); // reenable 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 = "") { // update the name this.filename = name; const fileValue = this.el?.querySelector("[data-file]"); if (fileValue) fileValue.textContent = this.filename; // show the file 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)); // fail randomly if (!this.isUploading) { return; } else if (Utils.randomInt(0,2) === 0) { this.fail(); return; } } // …or continue with progress 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); } }
In conclusion, the Animated File Upload Modal built with HTML, CSS, and JavaScript adds a clean, interactive way to handle file uploads. It improves user experience with stylish transitions and functional design.
If your project has problems, don’t worry. Just click to download the source code and face your coding challenges with excitement. Have fun coding!