Component

File Upload Input

File Upload Input.

  • Component
  • Vanilla
  • HTML
  • MIT

Preview

Live component

Preview not available

No interactive preview for this component.

Install it with the CLI below, or read the implementation on this page.

No dependencies Responsive · Focus-visible · Keyboard-ready

Install

Add to your project

terminal
npx devsnips add Vanilla/Components/Forms/file-upload-input
registry path: Vanilla/Components/Forms/file-upload-input

Source

Implementation

code.html
<!--
  File Upload Input with Drag-and-Drop UI
  - Drag and drop functionality
  - Multiple file support
  - File type validation
  - Progress indication
  - Accessible file selection
-->

<div class="file-upload-container">
  <div class="upload-area" 
       id="uploadArea"
       tabindex="0"
       role="button"
       aria-label="Drag and drop files here or click to select files"
  >
    <input type="file" 
           id="fileInput" 
           class="file-input" 
           multiple 
           aria-label="File upload"
    >
    
    <div class="upload-message">
      <svg viewBox="0 0 24 24" width="48" height="48">
        <path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z" fill="currentColor"/>
      </svg>
      <h3>Drag and drop files here</h3>
      <p>or <span class="browse-text">browse files</span></p>
      <p class="file-types">Supported files: Images, PDFs, Documents</p>
    </div>
    
    <div class="upload-progress" hidden>
      <div class="progress-circle">
        <svg viewBox="0 0 100 100">
          <circle class="progress-background" cx="50" cy="50" r="45"/>
          <circle class="progress-bar" cx="50" cy="50" r="45"/>
        </svg>
        <span class="progress-text">0%</span>
      </div>
      <p class="progress-status">Uploading...</p>
    </div>
  </div>
  
  <div class="file-list" role="list" aria-label="Uploaded files">
    <!-- Files will be listed here -->
  </div>
</div>

<style>
.file-upload-container {
  max-width: 600px;
  margin: 2rem auto;
  padding: 0 1rem;
}

.upload-area {
  position: relative;
  padding: 2rem;
  border: 2px dashed var(--ds-border, #ddd);
  border-radius: var(--ds-radius-md, 8px);
  background: var(--ds-surface, #fff);
  transition: all 0.3s;
  text-align: center;
  cursor: pointer;
}

.upload-area:hover,
.upload-area:focus {
  border-color: var(--ds-accent, #4a90e2);
  background: var(--ds-surface-2, #f8f9fa);
}

.upload-area.drag-over {
  border-color: var(--ds-accent, #4a90e2);
  background: var(--ds-accent-soft, #e3f2fd);
}

.file-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.upload-message {
  color: var(--ds-muted, #666);
}

.upload-message svg {
  margin-bottom: 1rem;
  color: var(--ds-subtle, #999);
}

.upload-message h3 {
  margin: 0 0 0.5rem;
  font-size: 1.25rem;
  color: var(--ds-foreground, #333);
}

.upload-message p {
  margin: 0;
}

.browse-text {
  color: var(--ds-accent, #4a90e2);
  text-decoration: underline;
}

.file-types {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  color: var(--ds-subtle, #999);
}

.upload-progress {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.9);
}

.progress-circle {
  position: relative;
  width: 80px;
  height: 80px;
  margin-bottom: 1rem;
}

.progress-circle svg {
  transform: rotate(-90deg);
}

.progress-background {
  fill: none;
  stroke: var(--ds-surface-3, #eee);
  stroke-width: 8;
}

.progress-bar {
  fill: none;
  stroke: var(--ds-accent, #4a90e2);
  stroke-width: 8;
  stroke-linecap: round;
  stroke-dasharray: 283;
  stroke-dashoffset: 283;
  transition: stroke-dashoffset 0.3s;
}

.progress-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 1.25rem;
  font-weight: 500;
  color: var(--ds-foreground, #333);
}

.progress-status {
  margin: 0;
  font-weight: 500;
  color: var(--ds-foreground, #333);
}

.file-list {
  margin-top: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.file-item {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.75rem;
  background: var(--ds-surface-2, #f8f9fa);
  border-radius: var(--ds-radius-sm, 4px);
  animation: slideIn 0.3s ease-out;
}

.file-icon {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ds-surface, #fff);
  border-radius: var(--ds-radius-sm, 4px);
  color: var(--ds-muted, #666);
}

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

.file-name {
  margin: 0 0 0.25rem;
  font-weight: 500;
  color: var(--ds-foreground, #333);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-size {
  margin: 0;
  font-size: 0.875rem;
  color: var(--ds-muted, #666);
}

.file-remove {
  flex-shrink: 0;
  padding: 0.5rem;
  border: none;
  background: none;
  color: var(--ds-muted, #666);
  cursor: pointer;
  transition: color 0.3s;
}

.file-remove:hover {
  color: var(--ds-danger, #f44336);
}

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

/* Focus styles */
.upload-area:focus-visible {
  outline: none;
  border-color: var(--ds-accent, #4a90e2);
  box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}

.file-remove:focus-visible {
  outline: 2px solid var(--ds-accent, #4a90e2);
  outline-offset: 2px;
  border-radius: var(--ds-radius-sm, 4px);
}

/* Mobile optimization */
@media (max-width: 600px) {
  .upload-area {
    padding: 1.5rem;
  }
  
  .upload-message svg {
    width: 40px;
    height: 40px;
  }
  
  .file-item {
    padding: 0.5rem;
  }
}

/* devsnips-qa:quality-bar */
@media (prefers-reduced-motion: reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}


/* DevSnips design tokens (Swiss) — edit tokens.css to re-theme */
:root{
--ds-bg:#fff;--ds-surface:#fff;--ds-surface-2:#f7f7f7;--ds-surface-3:#ededed;--ds-foreground:#0a0a0a;--ds-muted:#525252;--ds-subtle:#a3a3a3;--ds-border:#e5e5e5;--ds-border-strong:#d4d4d4;--ds-accent:#2563eb;--ds-accent-hover:#1d4ed8;--ds-accent-fg:#fff;--ds-accent-soft:#eff6ff;--ds-success:#16a34a;--ds-success-soft:#f0fdf4;--ds-warning:#d97706;--ds-warning-soft:#fffbeb;--ds-danger:#dc2626;--ds-danger-soft:#fef2f2;--ds-info:#2563eb;--ds-info-soft:#eff6ff;--ds-font-sans:system-ui,-apple-system,'Segoe UI',Roboto,Inter,'Helvetica Neue',Arial,sans-serif;--ds-font-mono:ui-monospace,'SF Mono','JetBrains Mono',Menlo,Consolas,monospace;--ds-text-xs:.75rem;--ds-text-sm:.875rem;--ds-text-base:1rem;--ds-text-lg:1.125rem;--ds-text-xl:1.25rem;--ds-text-2xl:1.5rem;--ds-text-3xl:1.875rem;--ds-text-4xl:2.25rem;--ds-leading-tight:1.25;--ds-leading-normal:1.5;--ds-leading-relaxed:1.625;--ds-weight-normal:400;--ds-weight-medium:500;--ds-weight-semibold:600;--ds-weight-bold:700;--ds-space-0:0;--ds-space-px:1px;--ds-space-1:.25rem;--ds-space-2:.5rem;--ds-space-3:.75rem;--ds-space-4:1rem;--ds-space-5:1.25rem;--ds-space-6:1.5rem;--ds-space-8:2rem;--ds-space-10:2.5rem;--ds-space-12:3rem;--ds-space-16:4rem;--ds-radius-sm:.25rem;--ds-radius-md:.5rem;--ds-radius-lg:.75rem;--ds-radius-xl:1rem;--ds-radius-2xl:1.5rem;--ds-radius-full:9999px;--ds-shadow-sm:0 1px 2px 0 rgba(0,0,0,.05);--ds-shadow-md:0 4px 6px -1px rgba(0,0,0,.08),0 2px 4px -2px rgba(0,0,0,.05);--ds-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.05);--ds-duration-fast:120ms;--ds-duration-normal:200ms;--ds-duration-slow:300ms;--ds-ease-out:cubic-bezier(.16,1,.3,1);--ds-ease-in-out:cubic-bezier(.4,0,.2,1);--ds-ring:0 0 0 2px #2563eb;--ds-container:1200px;--ds-gutter:1rem;
}
@media (prefers-color-scheme: dark){:root{--ds-bg:#0a0a0a;--ds-surface:#111;--ds-surface-2:#171717;--ds-surface-3:#1f1f1f;--ds-foreground:#fafafa;--ds-muted:#a3a3a3;--ds-subtle:#737373;--ds-border:#262626;--ds-border-strong:#404040;--ds-accent:#3b82f6;--ds-accent-hover:#60a5fa;--ds-accent-fg:#fff;--ds-accent-soft:#172554;--ds-success:#22c55e;--ds-success-soft:#052e16;--ds-warning:#f59e0b;--ds-warning-soft:#422006;--ds-danger:#ef4444;--ds-danger-soft:#450a0a;--ds-info:#3b82f6;--ds-info-soft:#172554;--ds-ring:0 0 0 2px #3b82f6;}}
</style>

<script>
class FileUploader {
  constructor(container) {
    this.container = container;
    this.uploadArea = container.querySelector('.upload-area');
    this.fileInput = container.querySelector('.file-input');
    this.fileList = container.querySelector('.file-list');
    this.progressCircle = container.querySelector('.progress-bar');
    this.progressText = container.querySelector('.progress-text');
    this.progressStatus = container.querySelector('.progress-status');
    this.uploadProgress = container.querySelector('.upload-progress');
    
    this.maxFileSize = 5 * 1024 * 1024; // 5MB
    this.allowedTypes = [
      'image/jpeg',
      'image/png',
      'image/gif',
      'application/pdf',
      'application/msword',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
    ];
    
    this.bindEvents();
  }
  
  bindEvents() {
    // Drag and drop events
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
      this.uploadArea.addEventListener(eventName, (e) => {
        e.preventDefault();
        e.stopPropagation();
      });
    });
    
    ['dragenter', 'dragover'].forEach(eventName => {
      this.uploadArea.addEventListener(eventName, () => {
        this.uploadArea.classList.add('drag-over');
      });
    });
    
    ['dragleave', 'drop'].forEach(eventName => {
      this.uploadArea.addEventListener(eventName, () => {
        this.uploadArea.classList.remove('drag-over');
      });
    });
    
    // File selection events
    this.uploadArea.addEventListener('drop', (e) => {
      const files = e.dataTransfer.files;
      this.handleFiles(files);
    });
    
    this.fileInput.addEventListener('change', (e) => {
      const files = e.target.files;
      this.handleFiles(files);
    });
    
    // Keyboard accessibility
    this.uploadArea.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        this.fileInput.click();
      }
    });
  }
  
  handleFiles(files) {
    Array.from(files).forEach(file => {
      if (!this.validateFile(file)) return;
      this.uploadFile(file);
    });
  }
  
  validateFile(file) {
    if (!this.allowedTypes.includes(file.type)) {
      this.showError('File type not supported');
      return false;
    }
    
    if (file.size > this.maxFileSize) {
      this.showError('File size exceeds 5MB limit');
      return false;
    }
    
    return true;
  }
  
  uploadFile(file) {
    // Show progress UI
    this.uploadProgress.hidden = false;
    
    // Simulate file upload with progress
    let progress = 0;
    const interval = setInterval(() => {
      progress += 10;
      this.updateProgress(progress);
      
      if (progress >= 100) {
        clearInterval(interval);
        this.uploadProgress.hidden = true;
        this.addFileToList(file);
      }
    }, 200);
  }
  
  updateProgress(value) {
    const circumference = 2 * Math.PI * 45;
    const offset = circumference - (value / 100) * circumference;
    this.progressCircle.style.strokeDashoffset = offset;
    this.progressText.textContent = `${value}%`;
  }
  
  addFileToList(file) {
    const fileItem = document.createElement('div');
    fileItem.className = 'file-item';
    fileItem.setAttribute('role', 'listitem');
    
    fileItem.innerHTML = `
      <div class="file-icon">
        <svg viewBox="0 0 24 24" width="24" height="24">
          <path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" fill="currentColor"/>
        </svg>
      </div>
      <div class="file-info">
        <h4 class="file-name">${file.name}</h4>
        <p class="file-size">${this.formatFileSize(file.size)}</p>
      </div>
      <button type="button" class="file-remove" aria-label="Remove file">
        <svg viewBox="0 0 24 24" width="24" height="24">
          <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
        </svg>
      </button>
    `;
    
    fileItem.querySelector('.file-remove').addEventListener('click', () => {
      fileItem.remove();
    });
    
    this.fileList.appendChild(fileItem);
  }
  
  formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
  
  showError(message) {
    // Implement error notification here
    console.error(message);
  }
}

// Initialize uploader
new FileUploader(document.querySelector('.file-upload-container'));
</script>
440 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/forms/file-upload-input

Continue browsing

View all