Skip to main content

Command Palette

Search for a command to run...

Drag and Drop: Implement File Upload UI

Learn: Drag and Drop: Implement File Upload UI

Updated
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Drag and Drop File Upload UI with HTML5

Problem

Users need an intuitive way to upload files without clicking through file dialogs. Traditional file inputs are clunky and don't provide visual feedback. We need a modern, accessible drag-and-drop interface that handles multiple files, validates them, and provides real-time feedback.

Solution

Create a drag-and-drop zone that:

  • Accepts files via drag-and-drop and click
  • Validates file types and sizes
  • Shows visual feedback during drag operations
  • Displays upload progress
  • Handles errors gracefully
  • Works with accessibility standards

Code

HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drag & Drop File Upload</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>File Upload Manager</h1>

        <!-- Drop Zone -->
        <div class="drop-zone" id="dropZone" role="region" aria-label="File upload area">
            <svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                <polyline points="17 8 12 3 7 8"></polyline>
                <line x1="12" y1="3" x2="12" y2="15"></line>
            </svg>
            <h2>Drop files here or click to upload</h2>
            <p class="drop-zone-text">Supported: Images, PDFs, Documents (Max 10MB each)</p>
            <input 
                type="file" 
                id="fileInput" 
                multiple 
                accept=".jpg,.jpeg,.png,.pdf,.doc,.docx,.txt"
                hidden
                aria-label="Select files to upload"
            >
        </div>

        <!-- File List -->
        <div class="file-list" id="fileList">
            <h3>Uploaded Files</h3>
            <ul id="fileItems" class="file-items"></ul>
        </div>

        <!-- Upload Button -->
        <button id="uploadBtn" class="upload-button" disabled>
            Upload Files
        </button>
    </div>

    <script src="script.js"></script>
</body>
</html>

CSS Styling

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.container {
    background: white;
    border-radius: 12px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    padding: 40px;
    max-width: 600px;
    width: 100%;
}

h1 {
    color: #333;
    margin-bottom: 30px;
    text-align: center;
    font-size: 28px;
}

/* Drop Zone */
.drop-zone {
    border: 3px dashed #667eea;
    border-radius: 8px;
    padding: 40px 20px;
    text-align: center;
    cursor: pointer;
    transition: all 0.3s ease;
    background-color: #f8f9ff;
}

.drop-zone:hover {
    border-color: #764ba2;
    background-color: #f0f2ff;
    transform: translateY(-2px);
}

.drop-zone.drag-over {
    border-color: #764ba2;
    background-color: #e8ebff;
    box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);
    transform: scale(1.02);
}

.upload-icon {
    width: 60px;
    height: 60px;
    color: #667eea;
    margin-bottom: 15px;
    animation: float 3s ease-in-out infinite;
}

@keyframes float {
    0%, 100% { transform: translateY(0px); }
    50% { transform: translateY(-10px); }
}

.drop-zone h2 {
    color: #333;
    font-size: 20px;
    margin-bottom: 10px;
}

.drop-zone-text {
    color: #666;
    font-size: 14px;
}

/* File List */
.file-list {
    margin-top: 30px;
    display: none;
}

.file-list.active {
    display: block;
}

.file-list h3 {
    color: #333;
    margin-bottom: 15px;
    font-size: 16px;
}

.file-items {
    list-style: none;
}

.file-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px;
    background: #f5f5f5;
    border-radius: 6px;
    margin-bottom: 10px;
    animation: slideIn 0.3s ease;
}

@keyframes slideIn {
    from {
        opacity: 0;
        transform: translateX(-20px);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

.file-info {
    display: flex;
    align-items: center;
    gap: 12px;
    flex: 1;
    min-width: 0;
}

.file-icon {
    width: 32px;
    height: 32px;
    background: #667eea;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-size: 12px;
    font-weight: bold;
    flex-shrink: 0;
}

.file-details {
    flex: 1;
    min-width: 0;
}

.file-name {
    color: #333;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 14px;
}

.file-size {
    color: #999;
    font-size: 12px;
    margin-top: 4px;
}

.file-progress {
    width: 100%;
    height: 4px;
    background: #e0e0e0;
    border-radius: 2px;
    margin-top: 6px;
    overflow: hidden;
}

.progress-bar {
    height: 100%;
    background: linear-gradient(90deg, #667eea, #764ba2);
    width: 0%;
    transition: width 0.3s ease;
}

.file-status {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
}

.status-icon {
    width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.status-success {
    color: #4caf50;
}

.status-error {
    color: #f44336;
}

.status-loading {
    color: #667eea;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

.remove-btn {
    background: none;
    border: none;
    color: #f44336;
    cursor: pointer;
    font-size: 18px;
    padding: 4px 8px;
    border-radius: 4px;
    transition: background 0.2s;
}

.remove-btn:hover {
    background: rgba(244, 67, 54, 0.1);
}

/* Upload Button */
.upload-button {
    width: 100%;
    padding: 12px;
    margin-top: 20px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 6px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
}

.upload-button:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}

.upload-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.upload-button.loading {
    position: relative;
    color: transparent;
}

.upload-button.loading::after {
    content: '';
    position: absolute;
    width: 16px;
    height: 16px;
    top: 50%;
    left: 50%;
    margin-left: -8px;
    margin-top: -8px;
    border: 2px solid rgba(255, 255, 255, 0.3);
    border-radius: 50%;
    border-top-color: white;
    animation: spin 0.8s linear infinite;
}

/* Responsive */
@media (max-width: 600px) {
    .container {
        padding: 20px;
    }

    h1 {
        font-size: 24px;
    }

    .drop-zone {
        padding: 30px 15px;
    }

    .upload-icon {
        width: 48px;
        height: 48px;
    }
}

JavaScript Implementation

class FileUploadManager {
    constructor(dropZoneId, fileInputId, fileListId, uploadBtnId) {
        this.dropZone = document.getElementById(dropZoneId);
        this.fileInput = document.getElementById(fileInputId);
        this.fileList = document.getElementById(fileListId);
        this.uploadBtn = document.getElementById(uploadBtnId);
        this.fileItems = document.getElementById('fileItems');

        this.files = new Map();
        this.config = {
            maxSize: 10 * 1024 * 1024, // 10MB
            allowedTypes: ['image/jpeg', 'image/png', 'application/pdf', 
                          'application/msword', 
                          'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                          'text/plain'],
            allowedExtensions: ['.jpg', '.jpeg', '.png', '.pdf', '.doc', '.docx', '.txt']
        };

        this.init();
    }

    init() {
        this.setupEventListeners();
    }

    setupEventListeners() {
        // Click to upload
        this.dropZone.addEventListener('click', () => this.fileInput.click());
        this.fileInput.addEventListener('change', (e) => this.handleFiles(e.target.files));

        // Drag and drop events
        this.dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
        this.dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
        this.dropZone.addEventListener('drop', (e) => this.handleDrop(e));

        // Upload button
        this.uploadBtn.addEventListener('click', () => this.uploadFiles());

        // Prevent default drag behavior on document
        document.addEventListener('dragover', (e) => e.preventDefault());
        document.addEventListener('drop', (e) => e.preventDefault());
    }

    handleDragOver(e) {
        e.preventDefault();
        e.stopPropagation();
        this.dropZone.classList.add('drag-over');
    }

    handleDragLeave(e) {
        e.preventDefault();
        e.stopPropagation();

        // Only remove class if leaving the drop zone entirely
        if (e.target === this.dropZone) {
            this.dropZone.classList.remove('drag-over');
        }
    }

    handleDrop(e) {
        e.preventDefault();
        e.stopPropagation();
        this.dropZone.classList.remove('drag-over');

        const droppedFiles = e.dataTransfer.files;
        this.handleFiles(droppedFiles);
    }

    handleFiles(fileList) {
        Array.from(fileList).forEach(file => {
            const validation = this.validateFile(file);

            if (validation.valid) {
                const fileId = this.generateFileId();
                this.files.set(fileId, {
                    file,
                    id: fileId,
                    status: 'pending',
                    progress: 0
                });
                this.renderFile(fileId);
            } else {
                this.showError(file.name, validation.error);
            }
        });

        this.updateUI();
    }

    validateFile(file) {
        // Check file size
        if (file.size > this.config.maxSize) {
            return {
                valid: false,
                error: `File too large. Max size: 10MB`
            };
        }

        // Check file type
        const extension = '.' + file.name.split('.').pop().toLowerCase();
        if (!this.config.allowedExtensions.includes(extension)) {
            return {
                valid: false,
                error: `File type not allowed: ${extension}`
            };
        }

        return { valid: true };
    }

    generateFileId() {
        return `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }

    renderFile(fileId) {
        const fileData = this.files.get(fileId);
        const { file } = fileData;

        const li = document.createElement('li');
        li.className = 'file-item';
        li.id = fileId;

        const extension = file.name.split('.').pop().toUpperCase();
        const sizeInMB = (file.size / (1024 * 1024)).toFixed(2);

        li.innerHTML = `
            <div class="file-info">
                <div class="file-icon">${extension}</div>
                <div class="file-details">
                    <div class="file-name" title="${file.name}">${file.name}</div>
                    <div class="file-size">${sizeInMB} MB</div>
                    <div class="file-progress">
                        <div class="progress-bar" style="width: 0%"></div>
                    </div>
                </div>
            </div>
            <div class="file-status">
                <span class="status-icon">
                    <span class="status-loading">⏳</span>
                </span>
            </div>
            <button class="remove-btn" aria-label="Remove file">✕</button>
        `;

        const removeBtn = li.querySelector('.remove-btn');
        removeBtn.addEventListener('click', () => this.removeFile(fileId));

        this.fileItems.appendChild(li);
    }

    removeFile(fileId) {
        this.files.delete(fileId);
        const element = document.getElementById(fileId);
        if (element) {
            element.remove();
        }
        this.updateUI();
    }

    updateUI() {
        const hasFiles = this.files.size > 0;
        this.fileList.classList.toggle('active', hasFiles);
        this.uploadBtn.disabled = !hasFiles;
    }

    async uploadFiles() {
        if (this.files.size === 0) return;

        this.uploadBtn.classList.add('loading');
        this.uploadBtn.disabled = true;

        for (const [fileId, fileData] of this.files) {
            await this.uploadFile(fileId, fileData);
        }

        this.uploadBtn.classList.remove('loading');
        this.uploadBtn.disabled = true;
    }

    uploadFile(fileId, fileData) {
        return new Promise((resolve) => {
            const { file } = fileData;
            const formData = new FormData();
            formData.append('file', file);

            const xhr = new XMLHttpRequest();

            // Progress tracking
            xhr.upload.addEventListener('progress', (e) => {
                if (e.lengthComputable) {
                    const percentComplete = (e.loaded / e.total) * 100;
                    this.updateFileProgress(fileId, percentComplete);
                }
            });

            // Completion
            xhr.addEventListener('load', () => {
                if (xhr.status === 200) {
                    this.updateFileStatus(fileId, 'success', '✓');
                    fileData.status = 'success';
                } else {
                    this.updateFileStatus(fileId, 'error', '✗');
                    fileData.status = 'error';
                }
                resolve();
            });

            // Error
            xhr.addEventListener('error', () => {
                this.updateFileStatus(fileId, 'error', '✗');
                fileData.status = 'error';
                resolve();
            });

            // Send request
            xhr.open('POST', '/api/upload');
            xhr.send(formData);
        });
    }

    updateFileProgress(fileId, progress) {
        const element = document.getElementById(fileId);
        if (element) {
            const progressBar = element.querySelector('.progress-bar');
            progressBar.style.width = `${progress}%`;
            this.files.get(fileId).progress = progress;
        }
    }

    updateFileStatus(fileId, status, icon) {
        const element = document.getElementById(fileId);
        if (element) {
            const statusIcon = element.querySelector('.status-icon');
            statusIcon.innerHTML = `<span class="status-${status}">${icon}</span>`;
            statusIcon.className = `status-icon status-${status}`;
        }
    }

    showError(fileName, error) {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: #f44336;
            color: white;
            padding: 16px 24px;
            border-radius: 6px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 1000;
            animation: slideIn 0.3s ease;
        `;
        notification.textContent = `${fileName}: ${error}`;
        document.body.appendChild(notification);

        setTimeout(() => notification.remove(), 4000);
    }
}

// Initialize
document.addEventListener('DOMContentLoaded', () => {
    new FileUploadManager('dropZone', 'fileInput', 'fileList', 'uploadBtn');
});

Backend Example (Node