Skip to content

How to Work with Files and FileReader in JavaScript

How to use JavaScript's File and FileReader APIs to handle file uploads, read file contents as text, binary data, or Data URLs, and implement drag-and-drop file handling in the browser.

Users interact with files constantly: uploading photos, attaching documents, importing spreadsheets, dragging images into editors. JavaScript provides two core APIs for handling these interactions on the client side: the File object, which represents a file selected by the user, and FileReader, which reads the contents of that file into memory so your code can process it.

The File object gives you metadata about the file (name, size, type, last modified date) and, since it extends Blob, provides all the Blob methods you already know. FileReader is the classic event-based API for reading file contents as text, binary data, or Data URLs. Together with the HTML <input type="file"> element and the drag-and-drop API, these tools let you build rich file-handling experiences entirely in the browser, without uploading anything to a server.

This guide covers the File object and its relationship to Blob, how to use file inputs to let users select files, every FileReader method with practical examples, and how to implement drag-and-drop file uploads.

The File Object (Inherits from Blob)

A File object represents a file from the user's file system. It is a subclass of Blob, which means it inherits all Blob properties and methods while adding file-specific metadata.

File Properties

// Assuming a file was selected via <input type="file">
let fileInput = document.getElementById("fileInput");

fileInput.addEventListener("change", (event) => {
  let file = event.target.files[0];

  // Inherited from Blob
  console.log(file.size);             // 524288 (size in bytes)
  console.log(file.type);             // "image/png" (MIME type)

  // File-specific properties
  console.log(file.name);             // "vacation-photo.png"
  console.log(file.lastModified);     // 1710500000000 (timestamp in ms)
  console.log(file.lastModifiedDate); // Date object (deprecated, use lastModified)

  // File IS a Blob
  console.log(file instanceof Blob);  // true
  console.log(file instanceof File);  // true
});
Property Type Description
name string The file's name including extension
size number Size in bytes (inherited from Blob)
type string MIME type, e.g. "image/jpeg" (inherited from Blob)
lastModified number Last modification timestamp in milliseconds

Since File Extends Blob, All Blob Methods Work

Because File inherits from Blob, you can use every Blob method directly on a File object:

fileInput.addEventListener("change", async (event) => {
  let file = event.target.files[0];

  // Blob methods work on File objects
  let text = await file.text();          // Read as UTF-8 string
  let buffer = await file.arrayBuffer(); // Read as ArrayBuffer
  let stream = file.stream();            // Get a ReadableStream

  // Slicing works too
  let firstKB = file.slice(0, 1024, file.type);
  console.log(firstKB.size);            // 1024 (or less if file is smaller)

  // Create a Blob URL
  let url = URL.createObjectURL(file);
  console.log(url);                     // "blob:http://localhost/..."
});

Creating File Objects Programmatically

While File objects usually come from user input, you can create them manually using the File constructor:

// new File(fileParts, fileName, options?)
let file = new File(
  ["Hello, World!\nLine 2\nLine 3"],  // Array of parts (like Blob constructor)
  "greeting.txt",                     // File name
  {
    type: "text/plain",               // MIME type
    lastModified: Date.now()          // Last modified timestamp
  }
);

console.log(file.name);               // "greeting.txt"
console.log(file.size);               // 28
console.log(file.type);               // "text/plain"
console.log(file.lastModified);       // current timestamp

// You can read it like any other File
let text = await file.text();
console.log(text);                    // "Hello, World!\nLine 2\nLine 3"

This is useful for testing, creating files to upload programmatically, or converting a Blob to a File (which adds a name):

// Convert a Blob to a File (adding a name)
let blob = new Blob(["CSV data here"], { type: "text/csv" });
let file = new File([blob], "data.csv", { type: blob.type });

console.log(file.name);             // "data.csv"
console.log(file instanceof File);  // true

// Upload it via fetch
let formData = new FormData();
formData.append("file", file);      // FormData needs a File, not a Blob, for the filename

File Input: <input type="file">

The <input type="file"> element is the standard way for users to select files from their device. When the user selects one or more files, the input's files property contains a FileList of File objects.

Basic File Input

<input type="file" id="singleFile">
let input = document.getElementById("singleFile");

input.addEventListener("change", (event) => {
  let file = event.target.files[0]; // First (and only) selected file

  if (!file) {
    console.log("No file selected");
    return;
  }

  console.log("Name:", file.name);
  console.log("Size:", (file.size / 1024).toFixed(1), "KB");
  console.log("Type:", file.type);
  console.log("Last Modified:", new Date(file.lastModified).toLocaleString());
});

Multiple File Selection

Add the multiple attribute to allow selecting more than one file:

<input type="file" id="multipleFiles" multiple>
let input = document.getElementById("multipleFiles");

input.addEventListener("change", (event) => {
  let files = event.target.files; // FileList (array-like)

  console.log(`${files.length} file(s) selected`);

  for (let file of files) {
    console.log(`- ${file.name} (${(file.size / 1024).toFixed(1)} KB, ${file.type})`);
  }

  // Convert FileList to a real array for array methods
  let fileArray = Array.from(files);
  let totalSize = fileArray.reduce((sum, f) => sum + f.size, 0);
  console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
});

Filtering by File Type

The accept attribute restricts which file types the user can select. It does not prevent all invalid files (the user can override it), so always validate in JavaScript too:

<!-- Accept only images -->
<input type="file" accept="image/*">

<!-- Accept only specific image formats -->
<input type="file" accept="image/png, image/jpeg, image/webp">

<!-- Accept only PDF files -->
<input type="file" accept=".pdf, application/pdf">

<!-- Accept documents -->
<input type="file" accept=".doc, .docx, .pdf, .txt">

<!-- Accept CSV and Excel -->
<input type="file" accept=".csv, .xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">

Selecting Directories

The webkitdirectory attribute lets users select an entire directory:

<input type="file" id="dirInput" webkitdirectory>
let dirInput = document.getElementById("dirInput");

dirInput.addEventListener("change", (event) => {
  let files = event.target.files;

  for (let file of files) {
    // webkitRelativePath shows the path relative to the selected directory
    console.log(file.webkitRelativePath, file.size);
  }
  // Example output:
  // "my-project/index.html" 1234
  // "my-project/styles/main.css" 5678
  // "my-project/scripts/app.js" 9012
});

Validating Files

Always validate files in JavaScript, regardless of the accept attribute:

function validateFile(file, options = {}) {
  let errors = [];

  // Check file type
  if (options.allowedTypes && options.allowedTypes.length > 0) {
    let isAllowed = options.allowedTypes.some(type => {
      if (type.endsWith("/*")) {
        // Wildcard: "image/*" matches "image/png", "image/jpeg", etc.
        return file.type.startsWith(type.slice(0, -1));
      }
      if (type.startsWith(".")) {
        // Extension: ".pdf" matches files ending in .pdf
        return file.name.toLowerCase().endsWith(type.toLowerCase());
      }
      return file.type === type;
    });

    if (!isAllowed) {
      errors.push(`File type "${file.type || "unknown"}" is not allowed`);
    }
  }

  // Check file size
  if (options.maxSize && file.size > options.maxSize) {
    let maxMB = (options.maxSize / 1024 / 1024).toFixed(1);
    let fileMB = (file.size / 1024 / 1024).toFixed(1);
    errors.push(`File is ${fileMB} MB, maximum is ${maxMB} MB`);
  }

  if (options.minSize && file.size < options.minSize) {
    errors.push(`File is too small (${file.size} bytes)`);
  }

  return {
    valid: errors.length === 0,
    errors
  };
}

// Usage
let input = document.getElementById("imageInput");
input.addEventListener("change", (event) => {
  let file = event.target.files[0];
  if (!file) return;

  let result = validateFile(file, {
    allowedTypes: ["image/png", "image/jpeg", "image/webp"],
    maxSize: 5 * 1024 * 1024, // 5 MB
  });

  if (!result.valid) {
    alert("Invalid file:\n" + result.errors.join("\n"));
    input.value = ""; // Clear the input
    return;
  }

  console.log("File is valid, proceeding...");
});

Programmatically Triggering File Selection

You can trigger the file dialog from JavaScript without the user clicking the actual input element. This is useful for custom-styled upload buttons:

<button id="uploadBtn">Choose Photo</button>
<input type="file" id="hiddenInput" accept="image/*" style="display: none;">
let uploadBtn = document.getElementById("uploadBtn");
let hiddenInput = document.getElementById("hiddenInput");

uploadBtn.addEventListener("click", () => {
  hiddenInput.click(); // Opens the file dialog
});

hiddenInput.addEventListener("change", (event) => {
  let file = event.target.files[0];
  if (file) {
    console.log("Selected:", file.name);
  }
});

Resetting a File Input

To clear a file input and allow re-selecting the same file:

// Clear the input
fileInput.value = "";
// or
fileInput.value = null;

// Now the "change" event will fire even if the user selects the same file again

FileReader: Reading File Contents

FileReader is an event-based API for asynchronously reading the contents of File or Blob objects. It provides methods for reading data in different formats: as text, as a Data URL (base64), or as an ArrayBuffer.

FileReader Lifecycle

Every FileReader operation follows the same pattern:

  1. Create a FileReader instance.
  2. Attach event handlers (onload, onerror, etc.).
  3. Call a read method (readAsText, readAsDataURL, readAsArrayBuffer).
  4. Wait for the result via events.
let reader = new FileReader();

// Events (in order of the lifecycle)
reader.onloadstart = () => console.log("Started reading");
reader.onprogress = (event) => {
  if (event.lengthComputable) {
    let percent = (event.loaded / event.total * 100).toFixed(1);
    console.log(`Progress: ${percent}%`);
  }
};
reader.onload = () => {
  console.log("Reading complete!");
  console.log("Result:", reader.result); // The file contents
};
reader.onerror = () => {
  console.error("Error:", reader.error);
};
reader.onloadend = () => {
  console.log("Finished (success or failure)");
};

// Start reading
reader.readAsText(file);

FileReader Events

Event When It Fires
loadstart Reading starts
progress Periodically during reading (for large files)
load Reading completed successfully. Result is in reader.result.
error An error occurred. Details in reader.error.
abort Reading was cancelled via reader.abort().
loadend Reading finished (after load, error, or abort). Always fires.

FileReader Properties

let reader = new FileReader();

// Before reading:
console.log(reader.readyState); // 0 (EMPTY)
console.log(reader.result);     // null

// During reading:
// reader.readyState === 1 (LOADING)

// After reading:
// reader.readyState === 2 (DONE)
// reader.result contains the data
// reader.error is null (on success) or a DOMException (on failure)
Constant Value Description
FileReader.EMPTY 0 No data loaded yet
FileReader.LOADING 1 Data is being read
FileReader.DONE 2 Reading is complete

readAsText, readAsDataURL, readAsArrayBuffer

FileReader provides three main methods for reading file contents, each producing the result in a different format.

readAsText(blob, encoding?): Read as String

Reads the file as a text string. The optional encoding parameter specifies the character encoding (defaults to UTF-8):

let input = document.getElementById("textFileInput");

input.addEventListener("change", (event) => {
  let file = event.target.files[0];
  if (!file) return;

  let reader = new FileReader();

  reader.onload = () => {
    let text = reader.result; // String
    console.log("File contents:", text);
    console.log("Length:", text.length, "characters");

    // Display in a textarea
    document.getElementById("output").value = text;
  };

  reader.onerror = () => {
    console.error("Error reading file:", reader.error.message);
  };

  reader.readAsText(file); // Default: UTF-8
});

Reading with a specific encoding:

// Read a file encoded in Windows-1251 (Cyrillic)
reader.readAsText(file, "windows-1251");

// Read a file encoded in Shift_JIS (Japanese)
reader.readAsText(file, "shift_jis");

// Read a file encoded in ISO-8859-1 (Latin-1)
reader.readAsText(file, "iso-8859-1");

Practical example: CSV file parser

function parseCSVFile(file) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();

    reader.onload = () => {
      let text = reader.result;
      let lines = text.split("\n").filter(line => line.trim());

      let headers = lines[0].split(",").map(h => h.trim());
      let rows = lines.slice(1).map(line => {
        let values = line.split(",").map(v => v.trim());
        let row = {};
        headers.forEach((header, i) => {
          row[header] = values[i] || "";
        });
        return row;
      });

      resolve({ headers, rows });
    };

    reader.onerror = () => reject(reader.error);
    reader.readAsText(file);
  });
}

// Usage
input.addEventListener("change", async (event) => {
  let file = event.target.files[0];
  let { headers, rows } = await parseCSVFile(file);

  console.log("Headers:", headers);
  console.table(rows);
});

readAsDataURL(blob): Read as Base64 Data URL

Reads the file and produces a Data URL string in the format data:[type];base64,[encoded data]. This is the standard way to display user-selected images immediately without uploading them to a server:

let imageInput = document.getElementById("imageInput");
let preview = document.getElementById("preview");

imageInput.addEventListener("change", (event) => {
  let file = event.target.files[0];
  if (!file || !file.type.startsWith("image/")) return;

  let reader = new FileReader();

  reader.onload = () => {
    let dataUrl = reader.result; // "data:image/png;base64,iVBORw0KGgo..."

    // Use directly as image source
    preview.src = dataUrl;
    console.log("Data URL length:", dataUrl.length, "characters");
  };

  reader.readAsDataURL(file);
});

Multiple image preview:

let multiInput = document.getElementById("multiImageInput");
let gallery = document.getElementById("gallery");

multiInput.addEventListener("change", (event) => {
  gallery.innerHTML = ""; // Clear previous previews

  for (let file of event.target.files) {
    if (!file.type.startsWith("image/")) continue;

    let reader = new FileReader();

    reader.onload = () => {
      let img = document.createElement("img");
      img.src = reader.result;
      img.style.cssText = "max-width: 200px; max-height: 200px; margin: 8px; border-radius: 8px;";
      gallery.append(img);
    };

    reader.readAsDataURL(file);
  }
});

readAsArrayBuffer(blob): Read as Raw Binary

Reads the file as an ArrayBuffer, giving you access to the raw bytes. Use this when you need to process binary file formats, inspect file headers, or perform byte-level operations:

let fileInput = document.getElementById("binaryInput");

fileInput.addEventListener("change", (event) => {
  let file = event.target.files[0];
  if (!file) return;

  let reader = new FileReader();

  reader.onload = () => {
    let buffer = reader.result; // ArrayBuffer
    let bytes = new Uint8Array(buffer);

    console.log("File size:", buffer.byteLength, "bytes");
    console.log("First 10 bytes:", Array.from(bytes.slice(0, 10)));

    // Detect file type from magic bytes
    let type = detectFileType(bytes);
    console.log("Detected type:", type);
  };

  reader.readAsArrayBuffer(file);
});

function detectFileType(bytes) {
  if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
    return "PNG";
  }
  if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
    return "JPEG";
  }
  if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
    return "GIF";
  }
  if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) {
    return "PDF";
  }
  if (bytes[0] === 0x50 && bytes[1] === 0x4B) {
    return "ZIP";
  }
  return "Unknown";
}

Practical example: Reading EXIF data from a JPEG

function readJPEGDimensions(file) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();

    reader.onload = () => {
      let view = new DataView(reader.result);

      // Check JPEG magic bytes
      if (view.getUint16(0) !== 0xFFD8) {
        reject(new Error("Not a JPEG file"));
        return;
      }

      // Walk through JPEG segments to find SOF (Start of Frame)
      let offset = 2;
      while (offset < view.byteLength) {
        let marker = view.getUint16(offset);

        if (marker === 0xFFC0 || marker === 0xFFC2) {
          // SOF0 or SOF2: contains dimensions
          let height = view.getUint16(offset + 5);
          let width = view.getUint16(offset + 7);
          resolve({ width, height });
          return;
        }

        // Skip to next segment
        let segmentLength = view.getUint16(offset + 2);
        offset += 2 + segmentLength;
      }

      reject(new Error("Could not find image dimensions"));
    };

    reader.onerror = () => reject(reader.error);
    reader.readAsArrayBuffer(file);
  });
}

// Usage
imageInput.addEventListener("change", async (event) => {
  let file = event.target.files[0];
  try {
    let { width, height } = await readJPEGDimensions(file);
    console.log(`Image dimensions: ${width} x ${height}`);
  } catch (error) {
    console.error(error.message);
  }
});

Method Comparison

Method reader.result Type Best For
readAsText(blob, encoding?) string Text files, CSV, JSON, HTML, code
readAsDataURL(blob) string (Data URL) Image previews, embedding in HTML/CSS, storing as string
readAsArrayBuffer(blob) ArrayBuffer Binary files, file format parsing, hashing, byte-level processing

Wrapping FileReader in Promises

The event-based API of FileReader becomes verbose quickly. Wrapping it in Promises produces cleaner code:

function readAsText(blob, encoding = "utf-8") {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsText(blob, encoding);
  });
}

function readAsDataURL(blob) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(blob);
  });
}

function readAsArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsArrayBuffer(blob);
  });
}

// Clean async usage
async function processFile(file) {
  if (file.type.startsWith("text/")) {
    let text = await readAsText(file);
    console.log("Text content:", text);
  } else if (file.type.startsWith("image/")) {
    let dataUrl = await readAsDataURL(file);
    document.getElementById("preview").src = dataUrl;
  } else {
    let buffer = await readAsArrayBuffer(file);
    let bytes = new Uint8Array(buffer);
    console.log("Binary, first bytes:", bytes.slice(0, 20));
  }
}

Tracking Progress for Large Files

For large files, the progress event gives you real-time feedback:

function readWithProgress(file, method = "readAsArrayBuffer") {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();

    reader.onprogress = (event) => {
      if (event.lengthComputable) {
        let percent = (event.loaded / event.total * 100).toFixed(1);
        let loadedMB = (event.loaded / 1024 / 1024).toFixed(1);
        let totalMB = (event.total / 1024 / 1024).toFixed(1);
        console.log(`Reading: ${percent}% (${loadedMB}/${totalMB} MB)`);

        // Update a progress bar
        let progressBar = document.getElementById("progress");
        if (progressBar) {
          progressBar.style.width = `${percent}%`;
          progressBar.textContent = `${percent}%`;
        }
      }
    };

    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);

    reader[method](file);
  });
}

// Usage
let buffer = await readWithProgress(largeFile, "readAsArrayBuffer");

Aborting a Read

You can cancel an in-progress read with reader.abort():

let reader = new FileReader();
let abortBtn = document.getElementById("abortBtn");

reader.onload = () => console.log("Done:", reader.result.byteLength, "bytes");
reader.onabort = () => console.log("Read aborted by user");
reader.onerror = () => console.error("Error:", reader.error);

// Start reading a large file
reader.readAsArrayBuffer(veryLargeFile);

// User can abort
abortBtn.addEventListener("click", () => {
  reader.abort();
});

Drag-and-Drop File Upload

Drag-and-drop provides a more intuitive file selection experience than the standard file input. Users can drag files directly from their desktop or file manager into a designated area on the page.

The Drop Zone

<div id="dropZone" class="drop-zone">
  <p>Drag and drop files here</p>
  <p class="hint">or click to browse</p>
  <input type="file" id="fallbackInput" multiple hidden>
</div>

<style>
  .drop-zone {
    width: 400px;
    height: 200px;
    border: 3px dashed #ccc;
    border-radius: 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-family: system-ui, sans-serif;
    color: #666;
    cursor: pointer;
    transition: all 0.2s;
  }

  .drop-zone.dragover {
    border-color: #2196f3;
    background-color: #e3f2fd;
    color: #1565c0;
  }

  .drop-zone .hint {
    font-size: 14px;
    color: #999;
  }
</style>

Core Drag-and-Drop Events

Four events are essential for implementing drag-and-drop:

let dropZone = document.getElementById("dropZone");
let fallbackInput = document.getElementById("fallbackInput");

// Click to browse (fallback)
dropZone.addEventListener("click", () => {
  fallbackInput.click();
});

fallbackInput.addEventListener("change", (event) => {
  handleFiles(event.target.files);
});

// Prevent default drag behaviors on the entire document
// (prevents the browser from opening the file)
document.addEventListener("dragover", (event) => {
  event.preventDefault();
});

document.addEventListener("drop", (event) => {
  event.preventDefault();
});

// Highlight the drop zone when a file is dragged over it
dropZone.addEventListener("dragenter", (event) => {
  event.preventDefault();
  dropZone.classList.add("dragover");
});

dropZone.addEventListener("dragover", (event) => {
  event.preventDefault(); // Required to allow drop
  dropZone.classList.add("dragover");
});

dropZone.addEventListener("dragleave", (event) => {
  // Only remove highlight if we actually left the drop zone
  // (dragleave fires when entering child elements)
  if (!dropZone.contains(event.relatedTarget)) {
    dropZone.classList.remove("dragover");
  }
});

// Handle the drop
dropZone.addEventListener("drop", (event) => {
  event.preventDefault();
  dropZone.classList.remove("dragover");

  let files = event.dataTransfer.files; // FileList
  if (files.length > 0) {
    handleFiles(files);
  }
});

Processing Dropped Files

function handleFiles(fileList) {
  let files = Array.from(fileList);
  console.log(`${files.length} file(s) dropped`);

  files.forEach(file => {
    console.log(`- ${file.name} (${formatFileSize(file.size)}, ${file.type})`);
  });

  // Process each file based on type
  files.forEach(processFile);
}

function formatFileSize(bytes) {
  if (bytes === 0) return "0 B";
  let units = ["B", "KB", "MB", "GB"];
  let i = Math.floor(Math.log(bytes) / Math.log(1024));
  return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
}

async function processFile(file) {
  if (file.type.startsWith("image/")) {
    displayImagePreview(file);
  } else if (file.type === "text/plain" || file.type === "text/csv") {
    let text = await file.text();
    console.log("Text file content:", text.substring(0, 200));
  } else if (file.type === "application/json") {
    let text = await file.text();
    let data = JSON.parse(text);
    console.log("JSON data:", data);
  } else {
    console.log(`Binary file: ${file.name} (${file.type})`);
  }
}

function displayImagePreview(file) {
  let url = URL.createObjectURL(file);

  let container = document.createElement("div");
  container.style.cssText = "display: inline-block; margin: 8px; position: relative;";

  let img = document.createElement("img");
  img.src = url;
  img.style.cssText = "max-width: 200px; max-height: 150px; border-radius: 8px; display: block;";
  img.onload = () => URL.revokeObjectURL(url);

  let label = document.createElement("small");
  label.textContent = `${file.name} (${formatFileSize(file.size)})`;
  label.style.cssText = "display: block; text-align: center; margin-top: 4px; color: #666;";

  container.append(img, label);
  document.getElementById("previews").append(container);
}

Validating Dropped Files

dropZone.addEventListener("drop", (event) => {
  event.preventDefault();
  dropZone.classList.remove("dragover");

  let files = Array.from(event.dataTransfer.files);

  // Validate each file
  let allowedTypes = ["image/png", "image/jpeg", "image/webp"];
  let maxSize = 10 * 1024 * 1024; // 10 MB

  let validFiles = [];
  let errors = [];

  files.forEach(file => {
    if (!allowedTypes.includes(file.type)) {
      errors.push(`"${file.name}": type "${file.type}" not allowed`);
    } else if (file.size > maxSize) {
      errors.push(`"${file.name}": ${formatFileSize(file.size)} exceeds ${formatFileSize(maxSize)} limit`);
    } else {
      validFiles.push(file);
    }
  });

  if (errors.length > 0) {
    alert("Some files were rejected:\n" + errors.join("\n"));
  }

  if (validFiles.length > 0) {
    handleFiles(validFiles);
  }
});

Reading Dropped Directories

When a user drops a folder, you can access its contents using the DataTransferItem API:

dropZone.addEventListener("drop", async (event) => {
  event.preventDefault();
  dropZone.classList.remove("dragover");

  let items = event.dataTransfer.items;
  let allFiles = [];

  for (let item of items) {
    let entry = item.webkitGetAsEntry();
    if (entry) {
      let files = await readEntry(entry);
      allFiles.push(...files);
    }
  }

  console.log(`Found ${allFiles.length} files total`);
  allFiles.forEach(f => console.log(f.fullPath, formatFileSize(f.file.size)));
});

async function readEntry(entry, path = "") {
  if (entry.isFile) {
    let file = await new Promise(resolve => entry.file(resolve));
    return [{ file, fullPath: path + entry.name }];
  }

  if (entry.isDirectory) {
    let dirReader = entry.createReader();
    let entries = await new Promise(resolve => dirReader.readEntries(resolve));

    let files = [];
    for (let childEntry of entries) {
      let childFiles = await readEntry(childEntry, path + entry.name + "/");
      files.push(...childFiles);
    }
    return files;
  }

  return [];
}

Complete Practical Example

Here is a comprehensive file manager component that combines file input, drag-and-drop, validation, and preview:

<!DOCTYPE html>
<html>
<head>
  <title>File Manager</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }

    .upload-area {
      border: 3px dashed #ccc; border-radius: 12px; padding: 40px;
      text-align: center; cursor: pointer; transition: all 0.2s;
      margin: 20px 0;
    }
    .upload-area.dragover { border-color: #2196f3; background: #e3f2fd; }
    .upload-area p { margin: 8px 0; }
    .upload-area .primary { font-size: 18px; font-weight: bold; color: #333; }
    .upload-area .secondary { font-size: 14px; color: #999; }

    .file-list { list-style: none; padding: 0; }
    .file-item {
      display: flex; align-items: center; gap: 12px;
      padding: 12px; margin: 8px 0; background: #f8f9fa;
      border-radius: 8px; border: 1px solid #e0e0e0;
    }
    .file-item .preview {
      width: 60px; height: 60px; object-fit: cover; border-radius: 6px;
      background: #e0e0e0; display: flex; align-items: center;
      justify-content: center; font-size: 24px; flex-shrink: 0;
    }
    .file-item .preview img { width: 100%; height: 100%; object-fit: cover; border-radius: 6px; }
    .file-item .info { flex-grow: 1; }
    .file-item .name { font-weight: bold; word-break: break-all; }
    .file-item .meta { font-size: 13px; color: #666; margin-top: 4px; }
    .file-item .content-preview {
      font-size: 12px; color: #888; margin-top: 4px;
      max-height: 40px; overflow: hidden; font-family: monospace;
    }
    .file-item .remove {
      background: none; border: none; font-size: 20px;
      cursor: pointer; color: #999; padding: 8px; flex-shrink: 0;
    }
    .file-item .remove:hover { color: #f44336; }

    .stats { padding: 12px; background: #e8f5e9; border-radius: 8px; margin-top: 16px; font-size: 14px; }
  </style>
</head>
<body>
  <h1>File Manager</h1>

  <div class="upload-area" id="uploadArea">
    <p class="primary">Drop files here or click to browse</p>
    <p class="secondary">Images, text files, JSON, CSV (max 10 MB each)</p>
    <input type="file" id="fileInput" multiple hidden>
  </div>

  <ul class="file-list" id="fileList"></ul>
  <div class="stats" id="stats" hidden></div>

  <script>
    let uploadArea = document.getElementById("uploadArea");
    let fileInput = document.getElementById("fileInput");
    let fileList = document.getElementById("fileList");
    let statsEl = document.getElementById("stats");

    let managedFiles = [];

    // Click to browse
    uploadArea.addEventListener("click", () => fileInput.click());
    fileInput.addEventListener("change", (e) => addFiles(e.target.files));

    // Drag and drop
    document.addEventListener("dragover", e => e.preventDefault());
    document.addEventListener("drop", e => e.preventDefault());

    uploadArea.addEventListener("dragenter", (e) => {
      e.preventDefault();
      uploadArea.classList.add("dragover");
    });

    uploadArea.addEventListener("dragover", (e) => {
      e.preventDefault();
      uploadArea.classList.add("dragover");
    });

    uploadArea.addEventListener("dragleave", (e) => {
      if (!uploadArea.contains(e.relatedTarget)) {
        uploadArea.classList.remove("dragover");
      }
    });

    uploadArea.addEventListener("drop", (e) => {
      e.preventDefault();
      uploadArea.classList.remove("dragover");
      addFiles(e.dataTransfer.files);
    });

    // Core logic
    function addFiles(fileListObj) {
      let files = Array.from(fileListObj);
      let maxSize = 10 * 1024 * 1024;
      let errors = [];

      for (let file of files) {
        if (file.size > maxSize) {
          errors.push(`"${file.name}" exceeds 10 MB limit`);
          continue;
        }
        managedFiles.push(file);
        renderFileItem(file, managedFiles.length - 1);
      }

      if (errors.length > 0) {
        alert("Some files were skipped:\n" + errors.join("\n"));
      }

      updateStats();
      fileInput.value = "";
    }

    async function renderFileItem(file, index) {
      let li = document.createElement("li");
      li.className = "file-item";
      li.dataset.index = index;

      // Preview
      let previewEl = document.createElement("div");
      previewEl.className = "preview";

      if (file.type.startsWith("image/")) {
        let img = document.createElement("img");
        let url = URL.createObjectURL(file);
        img.src = url;
        img.onload = () => URL.revokeObjectURL(url);
        previewEl.append(img);
      } else if (file.type.startsWith("text/") || file.type === "application/json") {
        previewEl.textContent = "📄";
      } else if (file.type === "application/pdf") {
        previewEl.textContent = "📕";
      } else {
        previewEl.textContent = "📎";
      }

      // Info
      let infoEl = document.createElement("div");
      infoEl.className = "info";

      let nameEl = document.createElement("div");
      nameEl.className = "name";
      nameEl.textContent = file.name;

      let metaEl = document.createElement("div");
      metaEl.className = "meta";
      metaEl.textContent = `${formatSize(file.size)} · ${file.type || "unknown type"} · ${new Date(file.lastModified).toLocaleDateString()}`;

      infoEl.append(nameEl, metaEl);

      // Text preview for text files
      if (file.type.startsWith("text/") || file.type === "application/json") {
        try {
          let text = await file.text();
          let contentPreview = document.createElement("div");
          contentPreview.className = "content-preview";
          contentPreview.textContent = text.substring(0, 150) + (text.length > 150 ? "..." : "");
          infoEl.append(contentPreview);
        } catch (e) {
          // Ignore read errors for preview
        }
      }

      // Remove button
      let removeBtn = document.createElement("button");
      removeBtn.className = "remove";
      removeBtn.textContent = "✕";
      removeBtn.title = "Remove file";
      removeBtn.addEventListener("click", () => {
        managedFiles.splice(index, 1);
        li.remove();
        updateStats();
        // Re-index remaining items
        fileList.querySelectorAll(".file-item").forEach((item, i) => {
          item.dataset.index = i;
        });
      });

      li.append(previewEl, infoEl, removeBtn);
      fileList.append(li);
    }

    function updateStats() {
      if (managedFiles.length === 0) {
        statsEl.hidden = true;
        return;
      }

      let totalSize = managedFiles.reduce((sum, f) => sum + f.size, 0);
      let typeGroups = {};
      managedFiles.forEach(f => {
        let category = f.type.split("/")[0] || "other";
        typeGroups[category] = (typeGroups[category] || 0) + 1;
      });

      let typeSummary = Object.entries(typeGroups)
        .map(([type, count]) => `${count} ${type}`)
        .join(", ");

      statsEl.textContent = `${managedFiles.length} files · ${formatSize(totalSize)} total · ${typeSummary}`;
      statsEl.hidden = false;
    }

    function formatSize(bytes) {
      if (bytes === 0) return "0 B";
      let units = ["B", "KB", "MB", "GB"];
      let i = Math.floor(Math.log(bytes) / Math.log(1024));
      return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + " " + units[i];
    }
  </script>
</body>
</html>

This example demonstrates:

  • File object properties: name, size, type, lastModified
  • <input type="file"> with multiple and hidden input for custom styling
  • file.text() (modern Blob method) for text file previews
  • URL.createObjectURL(file) for image previews with proper revocation
  • Drag-and-drop with dragenter, dragover, dragleave, drop event handling
  • File validation (size limits)
  • event.preventDefault() on both dragover and drop
  • Dynamic file list management with add/remove functionality

Summary

The File and FileReader APIs let you work with user-selected files entirely on the client side.

File Object:

  • Inherits from Blob, adding name, lastModified, and webkitRelativePath.
  • All Blob methods work on Files: text(), arrayBuffer(), bytes(), stream(), slice().
  • Created by <input type="file">, drag-and-drop, or the File constructor.
  • File instanceof Blob is true.

<input type="file">:

  • event.target.files returns a FileList (array-like) of File objects.
  • multiple allows selecting multiple files.
  • accept filters file types (but always validate in JavaScript too).
  • webkitdirectory allows selecting directories.
  • Clear with input.value = "".

FileReader Methods:

Method Result Type Use Case
readAsText(blob, encoding?) string Text, CSV, JSON, source code
readAsDataURL(blob) string (Data URL) Image previews, embedding in HTML, localStorage
readAsArrayBuffer(blob) ArrayBuffer Binary formats, file headers, hashing

FileReader Key Points:

  • Event-based: use onload for results, onerror for failures, onprogress for large files.
  • Result is in reader.result after onload fires.
  • Wrap in Promises for cleaner async code.
  • Consider modern file.text() and file.arrayBuffer() as simpler alternatives when you do not need progress or abort.

Drag-and-Drop:

  • event.preventDefault() is required on both dragover and drop.
  • Files are in event.dataTransfer.files (a FileList).
  • Use dragenter/dragleave for visual feedback (highlight the drop zone).
  • Check event.relatedTarget in dragleave to avoid false triggers from child elements.
  • Use item.webkitGetAsEntry() for directory traversal.

Choosing How to Read Files:

Need Best Approach
Simple text reading file.text() (modern) or readAsText
Image preview display URL.createObjectURL(file) (most efficient)
Image data for localStorage readAsDataURL
Binary processing file.arrayBuffer() (modern) or readAsArrayBuffer
Large file progress FileReader with onprogress
Cancellable read FileReader with abort()
Navigation