TikTok Tutorial #33 - How to create an Upload Modal in CSS & Javascript

Learn with us how to create an Upload Modal in CSS & Javascript!

If you found us on TikTok on the following post, check out this article and copy-paste the full code!

Happy coding! 😻

@creative.tim

What useful tutorials would you like us to create next?

♬ original sound - Creative Tim

Contents:
1. HTML Code
2. CSS Code
3. Javascript Code

Get your code ⬇️

1. HTML Code

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

2. CSS Code

* {
	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: #243856;
	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;
	}
}

3. Javascript Code

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);
    }
}

I hope you did find this tutorial useful!

For more web development or UI/UX design tutorials, follow us on:

Other useful resources:

Alexandra Murtaza

Alexandra Murtaza