File Upload

Material Design 3 file upload component with drag-and-drop support, multiple files, and file previews

File Upload

The File Upload component provides an intuitive way for users to upload files with drag-and-drop support, multiple file selection, progress tracking, and preview capabilities. Designed with Material Design 3 principles.

Basic Usage

Basic File Upload

Drag and Drop

The file upload component is designed to work seamlessly with drag-and-drop functionality.

Drag and Drop Upload

Multiple Files

Enable multiple file selection using the multiple attribute:

Multiple File Upload

File List with Preview

Display uploaded files with information and remove functionality:

File Upload with Preview

Preview
vacation-photo.jpg
2.4 MB
📄
document.pdf
1.8 MB

Upload Progress

Show upload progress for individual files:

Upload Progress

📄
uploading-file.pdf
3.2 MB

With Browse Button

Add an explicit button for browsing files:

File Upload with Browse Button

States

Success State

Show successfully uploaded files:

Success State

uploaded-successfully.pdf
1.2 MB

Error State

Display errors for failed uploads:

Error State

⚠️
failed-upload.pdf
15.8 MB
File size exceeds 10MB limit

Disabled State

Disable file upload when needed:

Disabled State

Max Files Reached

Prevent additional uploads when file limit is reached:

Max Files Reached State

Variants

Compact Variant

A smaller dropzone for space-constrained layouts:

Compact Variant

Outlined Variant

A more subtle appearance with transparent background:

Outlined Variant

Complete Example

Here’s a comprehensive example with all features:

Complete File Upload Example

Best Practices

File Validation

Always validate files on both client and server:

function validateFile(file) {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
  const maxSize = 10 * 1024 * 1024; // 10MB

  if (!allowedTypes.includes(file.type)) {
    return { valid: false, error: 'File type not supported' };
  }

  if (file.size > maxSize) {
    return { valid: false, error: 'File size exceeds 10MB limit' };
  }

  return { valid: true };
}

Accessibility

  • Provide clear labels and instructions
  • Use aria-label for icon buttons
  • Ensure keyboard navigation works
  • Announce upload status to screen readers

Accessible File Upload

You can upload multiple files by selecting them or dragging them into the upload area

User Feedback

  • Show upload progress for large files
  • Display clear error messages
  • Provide success confirmation
  • Allow users to cancel uploads

Security

  • Validate file types on the server
  • Scan uploaded files for malware
  • Sanitize file names
  • Implement size limits
  • Use secure file storage

Framework Examples

React

import { useState, useRef } from 'react';

interface FileUploadProps {
  maxFiles?: number;
  maxSize?: number;
  accept?: string;
  onFilesSelected?: (files: File[]) => void;
}

export function FileUpload({
  maxFiles = 5,
  maxSize = 10 * 1024 * 1024,
  accept,
  onFilesSelected
}: FileUploadProps) {
  const [files, setFiles] = useState<File[]>([]);
  const [dragOver, setDragOver] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault();
    setDragOver(true);
  };

  const handleDragLeave = () => {
    setDragOver(false);
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    setDragOver(false);
    handleFiles(Array.from(e.dataTransfer.files));
  };

  const handleFiles = (newFiles: File[]) => {
    if (files.length + newFiles.length > maxFiles) {
      alert(`Maximum ${maxFiles} files allowed`);
      return;
    }

    const validFiles = newFiles.filter(file => file.size <= maxSize);
    const updatedFiles = [...files, ...validFiles];
    setFiles(updatedFiles);
    onFilesSelected?.(updatedFiles);
  };

  const removeFile = (index: number) => {
    const updatedFiles = files.filter((_, i) => i !== index);
    setFiles(updatedFiles);
    onFilesSelected?.(updatedFiles);
  };

  return (
    <div className={`file-upload ${files.length >= maxFiles ? 'file-upload-max-reached' : ''}`}>
      <label
        className={`file-upload-dropzone ${dragOver ? 'file-upload-dropzone-dragover' : ''}`}
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onDrop={handleDrop}
      >
        <div className="file-upload-icon">📁</div>
        <p className="file-upload-text">Drop files here or click to browse</p>
        <p className="file-upload-hint">Max {maxFiles} files, {maxSize / 1024 / 1024}MB each</p>
      </label>
      <input
        ref={inputRef}
        type="file"
        className="file-upload-input"
        multiple
        accept={accept}
        onChange={(e) => e.target.files && handleFiles(Array.from(e.target.files))}
      />

      {files.length > 0 && (
        <div className="file-upload-list">
          {files.map((file, index) => (
            <div key={index} className="file-upload-item">
              <div className="file-upload-item-icon">📄</div>
              <div className="file-upload-item-info">
                <div className="file-upload-item-name">{file.name}</div>
                <div className="file-upload-item-size">
                  {(file.size / 1024 / 1024).toFixed(2)} MB
                </div>
              </div>
              <button
                className="file-upload-item-remove"
                onClick={() => removeFile(index)}
                aria-label="Remove file"
              >

              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Vue

<template>
  <div
    class="file-upload"
    :class="{ 'file-upload-max-reached': files.length >= maxFiles }"
  >
    <label
      class="file-upload-dropzone"
      :class="{ 'file-upload-dropzone-dragover': dragOver }"
      @dragover.prevent="dragOver = true"
      @dragleave="dragOver = false"
      @drop.prevent="handleDrop"
    >
      <div class="file-upload-icon">📁</div>
      <p class="file-upload-text">Drop files here or click to browse</p>
      <p class="file-upload-hint">Max {{ maxFiles }} files, {{ maxSize / 1024 / 1024 }}MB each</p>
    </label>
    <input
      ref="fileInput"
      type="file"
      class="file-upload-input"
      multiple
      :accept="accept"
      @change="handleFileSelect"
    />

    <div v-if="files.length > 0" class="file-upload-list">
      <div
        v-for="(file, index) in files"
        :key="index"
        class="file-upload-item"
      >
        <div class="file-upload-item-icon">📄</div>
        <div class="file-upload-item-info">
          <div class="file-upload-item-name">{{ file.name }}</div>
          <div class="file-upload-item-size">
            {{ (file.size / 1024 / 1024).toFixed(2) }} MB
          </div>
        </div>
        <button
          class="file-upload-item-remove"
          @click="removeFile(index)"
          aria-label="Remove file"
        >

        </button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

interface Props {
  maxFiles?: number;
  maxSize?: number;
  accept?: string;
}

const props = withDefaults(defineProps<Props>(), {
  maxFiles: 5,
  maxSize: 10 * 1024 * 1024,
  accept: undefined,
});

const emit = defineEmits<{
  filesSelected: [files: File[]];
}>();

const files = ref<File[]>([]);
const dragOver = ref(false);
const fileInput = ref<HTMLInputElement>();

const handleDrop = (e: DragEvent) => {
  dragOver.value = false;
  if (e.dataTransfer?.files) {
    handleFiles(Array.from(e.dataTransfer.files));
  }
};

const handleFileSelect = (e: Event) => {
  const target = e.target as HTMLInputElement;
  if (target.files) {
    handleFiles(Array.from(target.files));
  }
};

const handleFiles = (newFiles: File[]) => {
  if (files.value.length + newFiles.length > props.maxFiles) {
    alert(`Maximum ${props.maxFiles} files allowed`);
    return;
  }

  const validFiles = newFiles.filter(file => file.size <= props.maxSize);
  files.value = [...files.value, ...validFiles];
  emit('filesSelected', files.value);
};

const removeFile = (index: number) => {
  files.value = files.value.filter((_, i) => i !== index);
  emit('filesSelected', files.value);
};
</script>

API Reference

Class Names

ClassDescription
.file-uploadBase container for file upload component
.file-upload-dropzoneDrop zone area for dragging files
.file-upload-dropzone-dragoverApplied when files are dragged over the dropzone
.file-upload-iconIcon displayed in the dropzone
.file-upload-textMain text in the dropzone
.file-upload-hintHelper text in the dropzone
.file-upload-inputHidden file input element
.file-upload-buttonBrowse button inside dropzone
.file-upload-listContainer for uploaded file items
.file-upload-itemIndividual file item
.file-upload-item-iconIcon for file item
.file-upload-item-thumbnailImage thumbnail for file preview
.file-upload-item-infoContainer for file name and size
.file-upload-item-nameFile name display
.file-upload-item-sizeFile size display
.file-upload-item-removeRemove button for file item
.file-upload-progressProgress bar container
.file-upload-progress-barProgress bar indicator
.file-upload-item-errorError state for file item
.file-upload-item-error-messageError message text
.file-upload-item-successSuccess state for file item
.file-upload-compactCompact variant with smaller dropzone
.file-upload-outlinedOutlined variant with transparent background
.file-upload-disabledDisabled state
.file-upload-max-reachedApplied when maximum files limit is reached

Input Attributes

AttributeDescription
type="file"Required for file input
multipleAllow multiple file selection
acceptSpecify allowed file types (e.g., "image/*,.pdf")
disabledDisable file upload

See Also